The rewrite. v0.20

This commit is contained in:
lllllllillllllillll 2024-01-07 18:29:56 -08:00
parent 2c8d9993c6
commit 0cbf9226e5
117 changed files with 5190 additions and 4252 deletions

1
.github/FUNDING.yml vendored
View file

@ -1,2 +1 @@
github: [lllllllillllllillll]
patreon: DweebUI

View file

@ -15,6 +15,6 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
labels:
- "🤖 Dependencies"

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules
database/database.sqlite
test
.dockerignore

View file

@ -1,4 +1,20 @@
## v0.09 (dev)
## v0.20 (dev) - The rewrite. Jumping all the way to v0.20.
* Changed to ES6 imports.
* Cleaned up file structure and code layout.
* Updated DweebUI logo.
* Visual tweaks to login and registration pages.
* Added .gitignore and .dockerignore files.
* Syslogs - View logs for sign-in and registration attempts. :new:
* Docker socket now uses default connection.
* Updated Users page displays 'inactive' if no sign-ins within 30 days.
* Dashboard updates now triggered by Docker events.
* Massive reduction in the amount of HTML, CSS, and JS on client side.
* Container graphs are significantly more efficent and no longer use localStorage.
* Made dark mode the default theme.
* Created intervals to allow application to idle or scale bettery with more users.
* Pages for images, volumes, and networks (non-functional at the moment). :new:
## <del>v0.09 (dev)</del> dead. (It had so many problems that I essentially rewrote everything)
* Added authentication middleware to router.
* Added gzip compression.
* Added PM2.

View file

@ -1,8 +1,7 @@
# syntax=docker/dockerfile:1
FROM node:21-alpine
ENV NODE_ENV=production
ENV LOGGER=true
WORKDIR /app

View file

@ -1,13 +1,18 @@
# DweebUI
DweebUI is a simple Docker web interface created using Javascript, Node.JS, and Express.
DweebUI is a web interface for managing Docker, with a zero-config dashboard for your containers.
Alpha v0.09 ( :fire: Experimental :fire: )
Alpha v0.20 ( :fire: Experimental :fire: )
:warning: DweebUI is a management interface and should not be directly exposed to the internet:warning:
:warning: External access should be done through a VPN or secure SSH connection :warning:
[![GitHub Stars](https://img.shields.io/github/stars/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
[![GitHub Activity](https://img.shields.io/github/commit-activity/y/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
[![Docker Pulls](https://img.shields.io/docker/pulls/lllllllillllllillll/dweebui)](https://hub.docker.com/repository/docker/lllllllillllllillll/dweebui)
[![GitHub License](https://img.shields.io/github/license/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
* This is a personal project that I started to get more familiar with Javascript and Node.js.
* I probably should have waited a lot longer to share this :|
<a href="https://raw.githubusercontent.com//lllllllillllllillll/DweebUI/main/screenshots/dashboard.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard.png" width="50%"/></a>
@ -37,12 +42,11 @@ Docker Compose:
```
version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.09-dev
# build:
# context: .
# image: lllllllillllllillll/dweebui:v0.20
build:
context: .
environment:
NODE_ENV: production
PORT: 8000
@ -52,21 +56,15 @@ services:
- 8000:8000
volumes:
- dweebui:/app
- caddyfiles:/app/caddyfiles
- /var/run/docker.sock:/var/run/docker.sock
#- ./custom-templates.json:/app/custom-templates.json
#- ./composefiles:/app/composefiles
networks:
- dweeb_network
- dweebui_net
volumes:
dweebui:
caddyfiles:
networks:
dweeb_network:
dweebui_net:
driver: bridge
```
@ -77,14 +75,6 @@ Compose setup:
* 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```.
Using setup.sh:
```
Extract DweebUI.zip and navigate to /DweebUI
cd DweebUI
chmod +x setup.sh
sudo ./setup.sh
```
## Credits

389
app.js
View file

@ -1,23 +1,43 @@
// Express
const express = require("express");
const app = express();
const session = require("express-session");
const compression = require('compression');
const helmet = require('helmet');
const PORT = process.env.PORT || 8000;
import express from 'express';
import session from 'express-session';
import compression from 'compression';
import helmet from 'helmet';
import Docker from 'dockerode';
import cors from 'cors';
import { Readable } from 'stream';
import { instrument } from '@socket.io/admin-ui'
import { router } from './router/index.js';
import { createServer } from 'node:http';
import { Server } from 'socket.io';
import { sequelize, Container } from './database/models.js';
import { currentLoad, mem, networkStats, fsSize, dockerContainerStats, dockerImages, networkInterfaces } from 'systeminformation';
import { containerCard } from './components/containerCard.js';
// Router
const routes = require("./routes");
export const app = express();
const server = createServer(app);
const port = process.env.PORT || 8000;
export var docker = new Docker();
let [cpu, ram, tx, rx, disk] = [0, 0, 0, 0, 0];
let [hidden, clicked, dockerEvents] = ['', false, ''];
let metricsInterval, cardsInterval, graphsInterval;
let cardList = '';
const statsArray = {};
// Functions and variables
const { serverStats, containerList, containerStats, containerAction, containerLogs, hiddenContainers, dockerImages, dockerVolumes, dockerNetworks } = require('./functions/system');
let sentList, clicked;
app.locals.site_list = '';
// socket.io admin ui
export const io = new Server(server, {
connectionStateRecovery: {},
cors: {
origin: ['http://localhost:8000', 'https://admin.socket.io'],
methods: ['GET', 'POST'],
credentials: true
}
});
instrument(io, {
auth: false,
readonly: true
});
const Containers = require('./database/ContainerModel');
// Configure Session
// Session middleware
const sessionMiddleware = session({
secret: "keyboard cat",
resave: false,
@ -27,136 +47,279 @@ const sessionMiddleware = session({
httpOnly:false, // Only set to true if you are using HTTPS.
maxAge:3600000 * 8 // Session max age in milliseconds. 3600000 = 1 hour.
}
})
});
io.engine.use(sessionMiddleware);
// Middleware
// Express middleware
app.set('view engine', 'ejs');
app.use([
compression(),
cors(),
helmet({contentSecurityPolicy: false}),
express.static("public"),
express.json(),
express.urlencoded({ extended: true }),
sessionMiddleware,
routes
router
]);
// Start Express server
const server = app.listen(PORT, async () => {
console.log(`App listening on port ${PORT}`);
// Initialize server
server.listen(port, () => {
async function init() {
try {
await sequelize.authenticate();
console.log('[Connected to DB]');
} catch (error) {
console.log('[Could not connect to DB]', error);
}
try {
await sequelize.sync();
console.log('[Models Synced]');
hidden = await Container.findAll({ where: {visibility:false}});
containerCards();
} catch (error) {
console.log('[Could not Sync Models]', error);
}
console.log(`\nServer listening on http://localhost:${port}`);
}
init();
});
// Start Socket.io
const io = require('socket.io')(server);
io.engine.use(sessionMiddleware);
// Server metrics
let serverMetrics = async () => {
currentLoad().then(data => {
cpu = Math.round(data.currentLoad);
});
mem().then(data => {
ram = Math.round((data.active / data.total) * 100);
});
networkStats().then(data => {
tx = data[0].tx_bytes / (1024 * 1024);
rx = data[0].rx_bytes / (1024 * 1024);
});
fsSize().then(data => {
disk = data[0].use;
});
}
// List docker containers
let containerCards = async () => {
let list = '';
const allContainers = await docker.listContainers({ all: true });
for (const container of allContainers) {
if (!hidden.includes(container.Names[0].slice(1))) {
let imageVersion = container.Image.split('/');
let service = imageVersion[imageVersion.length - 1].split(':')[0];
let containerId = docker.getContainer(container.Id);
let containerInfo = await containerId.inspect();
let ports_list = [];
try {
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
let ports = {
check: 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {}
let external_port = ports_list[0]?.external || 0;
let internal_port = ports_list[0]?.internal || 0;
let container_info = {
name: container.Names[0].slice(1),
service: service,
id: container.Id,
state: container.State,
image: container.Image,
external_port: external_port,
internal_port: internal_port,
ports: ports_list,
link: 'localhost',
}
let card = containerCard(container_info);
list += card;
}
}
cardList = list;
}
// Container metrics
let containerStats = async () => {
const data = await docker.listContainers({ all: true });
for (const container of data) {
if (!hidden.includes(container.Names[0].slice(1))) {
const stats = await dockerContainerStats(container.Id);
const name = container.Names[0].slice(1);
if (!statsArray[name]) {
statsArray[name] = {
cpuArray: Array(15).fill(0),
ramArray: Array(15).fill(0)
};
}
statsArray[name].cpuArray.push(Math.round(stats[0].cpuPercent));
statsArray[name].ramArray.push(Math.round(stats[0].memPercent));
statsArray[name].cpuArray = statsArray[name].cpuArray.slice(-15);
statsArray[name].ramArray = statsArray[name].ramArray.slice(-15);
}
}
}
// Store docker events
docker.getEvents((err, stream) => {
if (err) throw err;
stream.on('data', (chunk) => {
dockerEvents += chunk.toString('utf8');
});
});
// Check for docker events
setInterval( () => {
if (dockerEvents != '') {
getHidden();
containerCards();
dockerEvents = '';
}
}, 1000);
// Get hidden containers
async function getHidden() {
hidden = await Container.findAll({ where: {visibility:false}});
}
// Socket.io
io.on('connection', (socket) => {
let sessionData = socket.request.session;
let sent = '';
if (sessionData.user != undefined) {
console.log(`${sessionData.user} connected from ${socket.handshake.headers.host}`);
// Set user session
const user_session = socket.request.session;
console.log(`${user_session.user} connected from ${socket.handshake.headers.host} ${socket.handshake.address}`);
// Check if a list of containers or an install card needs to be sent
if (sentList != null) { socket.emit('cards', sentList); }
if((app.locals.install != '') && (app.locals.install != null)){ socket.emit('install', app.locals.install); }
async function dockerStuff(){
let i = await dockerImages();
let v = await dockerVolumes();
let n = await dockerNetworks();
// console.log(i, v, n);
// Start intervals if not already started
if (!metricsInterval) {
metricsInterval = setInterval(serverMetrics, 1000);
console.log('Metrics interval started');
}
if (!cardsInterval) {
cardsInterval = setInterval(containerCards, 1000);
console.log('Cards interval started');
}
if (!graphsInterval) {
graphsInterval = setInterval(containerStats, 1000);
console.log('Graphs interval started');
}
dockerStuff();
// Send server metrics
let ServerStats = setInterval(async () => {
socket.emit('metrics', await serverStats());
}, 1000);
// Send list of containers
let ContainerList = setInterval(async () => {
let cardList = await containerList();
if (sentList !== cardList) {
sentList = cardList;
app.locals.install = '';
socket.emit('cards', cardList);
setInterval(() => {
socket.emit('metrics', [cpu, ram, tx, rx, disk]);
if (sent != cardList) {
sent = cardList;
socket.emit('containers', cardList);
}
socket.emit('containerStats', statsArray);
}, 1000);
// Send container metrics
let ContainerStats = setInterval(async () => {
let stats = await containerStats();
for (let i = 0; i < stats.length; i++) {
socket.emit('containerStats', stats[i]);
}
}, 1000);
// Container controls
socket.on('clicked', (data) => {
if (clicked == true) { return; } clicked = true;
let buttonPress = {
user: socket.request.session.user,
role: socket.request.session.role,
action: data.action,
container: data.container,
state: data.state
let { name, id, value } = data;
console.log(`${sessionData.user} clicked: ${id} ${value} ${name}`);
if (id == 'logs'){
function containerLogs (data) {
return new Promise((resolve, reject) => {
let logString = '';
var options = {
follow: false,
stdout: true,
stderr: false,
timestamps: false
};
var containerName = docker.getContainer(data);
containerName.logs(options, function (err, stream) {
if (err) {
reject(err);
return;
}
const readableStream = Readable.from(stream);
readableStream.on('data', function (chunk) {
logString += chunk.toString('utf8');
});
readableStream.on('end', function () {
resolve(logString);
});
});
});
};
containerLogs(name).then((data) => {
socket.emit('logs', data);
}).catch((err) => {
console.log(err);
});
}
if (id == 'start' || id == 'stop' || id == 'pause' || id == 'restart'){
var containerName = docker.getContainer(name);
if ((id == 'start') && (value == 'stopped')) {
containerName.start();
} else if ((id == 'start') && (value == 'paused')) {
containerName.unpause();
} else if ((id == 'stop') && (value != 'stopped')) {
containerName.stop();
} else if ((id == 'pause') && (value == 'running')) {
containerName.pause();
} else if ((id == 'pause') && (value == 'paused')) {
containerName.unpause();
} else if (id == 'restart') {
containerName.restart();
}
}
containerAction(buttonPress);
clicked = false;
});
socket.on('hide', async (data) => {
console.log(`Hide ${data.container}`);
let containerExists = await Containers.findOne({ where: {name:data.container}});
if(!containerExists){
const container = await Containers.create({
name: data.container,
visibility: false,
socket.on('disconnect', () => {
console.log(`${sessionData.user} disconnected`);
socket.disconnect();
// clear intervals if no users are connected
if (io.engine.clientsCount == 0) {
clearInterval(metricsInterval);
clearInterval(cardsInterval);
clearInterval(graphsInterval);
metricsInterval = null;
cardsInterval = null;
graphsInterval = null;
console.log('All intervals cleared');
}
});
hiddenContainers();
console.log(`[Created] Container ${data.container} hidden`)
let containerData = await Containers.findOne({ where: {name:data.container}});
console.log(containerData);
} else {
containerExists.update({ visibility: false });
console.log(`[Updated] Container ${data.container} hidden`)
hiddenContainers();
console.log('Missing session data');
}
});
socket.on('reset', (data) => {
// set visibility to true for all containers
Containers.update({ visibility: true }, { where: {} });
console.log('All containers visible');
hiddenContainers();
});
// Container logs
socket.on('logs', (data) => {
containerLogs(data.container)
.then(logs => {
console.log(`Refreshed logs for ${data.container}`)
socket.emit('logString', logs);
})
.catch(err => {
console.error(err);
});
});
// On disconnect
socket.on('disconnect', () => {
clearInterval(ServerStats);
clearInterval(ContainerList);
clearInterval(ContainerStats);
});
});
// let link = '';
// networkInterfaces().then(data => {
// link = data[0].ip4;
// });

View file

@ -1 +0,0 @@
import ./sites/*

View file

@ -1,4 +1,4 @@
function appCard(data) {
export const appCard = (data) => {
// make data.title lowercase
let app_name = data.name || data.title.toLowerCase();
@ -227,11 +227,11 @@ function appCard(data) {
</div>
</div>
<div class="d-flex">
<a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-info"><!-- Download SVG icon from http://tabler-icons.io/i/mail -->
<a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-info">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-article" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path> <path d="M7 8h10"></path> <path d="M7 12h10"></path> <path d="M7 16h10"></path></svg>
  Learn More
</a>
<a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-install"><!-- Download SVG icon from http://tabler-icons.io/i/phone -->
<a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-install">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-bar-to-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M4 20l16 0"></path> <path d="M12 14l0 -10"></path> <path d="M12 14l4 -4"></path> <path d="M12 14l-4 -4"></path></svg>
  Install
</a>
@ -990,5 +990,3 @@ function appCard(data) {
}
module.exports = { appCard };

118
components/containerCard.js Normal file
View file

@ -0,0 +1,118 @@
// export for app.js
export const containerCard = (data) => {
let { name, service, state, external_port, internal_port, ports, link } = data;
let wrapped = name;
let chart = name;
if (name.length > 13) {
wrapped = name.slice(0, 10) + '...';
}
//disable controls for a docker container depending on its name
let actions = "";
if (name.startsWith('dweebui')) {
actions = 'disabled=""';
}
if ( external_port == undefined ) { external_port = 0; }
if ( internal_port == undefined ) { internal_port = 0; }
let state_indicator = 'green';
if (state == 'exited') {
state = 'stopped';
state_indicator = 'red';
} else if (state == 'paused') {
state_indicator = 'orange';
}
let ports_data = [];
if (ports) {
ports_data = ports;
} else {
for (let i = 0; i < 12; i++) {
let port_check = "checked";
let external = i;
let internal = i;
let protocol = "tcp";
ports_data.push({
check: port_check,
external: external,
internal: internal,
protocol: protocol
});
}
}
return `
<div class="col-sm-6 col-lg-3 deleteme">
<div class="card">
<div class="card-body">
<div class="card-stamp card-stamp-sm">
<img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service}.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
</div>
<div class="d-flex align-items-center">
<div class="subheader text-yellow">${external_port}:${internal_port}</div>
<div class="ms-auto lh-1">
<div class="card-actions btn-actions">
<div class="card-actions btn-actions">
<button onclick="clicked(this)" name="${name}" value="${state}" id="start" class="btn-action" title="Start" ${actions}><!-- player-play -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
</button>
<button onclick="clicked(this)" name="${name}" value="${state}" id="stop" class="btn-action" title="Stop" ${actions}><!-- player-stop -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
</button>
<button onclick="clicked(this)" name="${name}" value="${state}" id="pause" class="btn-action" title="Pause" ${actions}><!-- player-pause -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
</button>
<button onclick="clicked(this)" name="${name}" value="${state}" id="restart" class="btn-action" title="Restart" ${actions}><!-- reload -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>
</button>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" onclick="clicked(this)" id="details" data-bs-toggle="modal" data-bs-target="#details_modal" href="#">Details</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="logs" data-bs-toggle="modal" data-bs-target="#log_view" href="#">Logs</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="edit" href="#">Edit</button>
<button class="dropdown-item text-primary" onclick="clicked(this)" name="${name}" id="update" href="#">Update</button>
<button class="dropdown-item text-danger" onclick="clicked(this)" name="${name}" id="remove" data-bs-toggle="modal" data-bs-target="#remove_modal" href="#">Remove</button>
</div>
</div>
<div class="dropdown">
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
</a>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item" onclick="clicked(this)" name="${name}" id="hide" value="hide">Hide</button>
<button class="dropdown-item" onclick="clicked(this)" name="${name}" id="resetView" value="resetView">Reset View</button>
<button class="dropdown-item" onclick="clicked(this)" name="${name}" id="permissions" value="permissions">Permissions</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex align-items-baseline">
<div class="h1 me-2" title="${name}" style="margin-bottom: 0;">
<a href="http://${link}:${external_port}" target="_blank">
${wrapped}
</a>
</div>
<div class="ms-auto">
<span class="text-${state_indicator} align-items-center lh-1">
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
${state}
</span>
</div>
</div>
<div id="${chart}_chart" class="chart-sm"></div>
</div>
</div>
</div>`;
}

File diff suppressed because it is too large Load diff

View file

@ -1,18 +0,0 @@
function siteCard(type, domain, host, port, id) {
let site = `<tr>`
site += `<td><input class="form-check-input m-0 align-middle" name="select${id}" value="${domain}" type="checkbox" aria-label="Select invoice"></td>`
site += `<td><span class="text-muted">${id}</span></td>`
site += `<td><a href="https://${domain}" class="text-reset" tabindex="-1" target="_blank">${domain}</a></td>`
site += `<td>${type}</td>`
site += `<td>${host}</td>`
site += `<td>${port}</td>`
site += `<td><span class="badge bg-success me-1"></span> Enabled</td>`
site += `<td><span class="badge bg-success me-1"></span> Enabled</td>`
site += `<td class="text-end"><a class="btn" href="#"> Edit </a></td>`
site += `</tr>`
return site;
}
module.exports = { siteCard };

View file

@ -1,22 +1,19 @@
const User = require('../database/UserModel');
import { User } from "../database/models.js";
export const Account = async (req, res) => {
exports.Account = async function(req, res) {
if (req.session.user) {
// Get the user.
let user = await User.findOne({ where: { UUID: req.session.UUID }});
// Render the home page
res.render("pages/account", {
first_name: user.first_name,
last_name: user.last_name,
name: user.first_name + ' ' + user.last_name,
res.render("account", {
first_name: user.name,
last_name: user.name,
name: user.name,
id: user.id,
email: user.email,
role: user.role,
avatar: user.avatar,
isLoggedIn: true
});
} else {
// Redirect to the login page
res.redirect("/login");
}
}

View file

@ -1,20 +1,16 @@
const { appCard } = require('../components/appCard')
const { dashCard } = require('../components/dashCard');
const { install, uninstall } = require('../functions/package_manager');
import { readFileSync } from 'fs';
import { appCard } from '../components/appCard.js';
const templates_json = require('../templates.json');
let templates = templates_json.templates;
let templatesJSON = readFileSync('./templates.json');
let templates = JSON.parse(templatesJSON).templates;
// sort templates alphabetically
templates = templates.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
});
exports.Apps = async function(req, res) {
export const Apps = (req, res) => {
let page = Number(req.params.page) || 1;
let list_start = (page-1)*28;
let list_end = (page*28);
@ -36,12 +32,10 @@ exports.Apps = async function(req, res) {
apps_list += app_card;
}
// Render the home page
res.render("pages/apps", {
res.render("apps", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
isLoggedIn: true,
list_start: list_start + 1,
list_end: list_end,
app_count: templates.length,
@ -54,7 +48,7 @@ exports.Apps = async function(req, res) {
exports.searchApps = async function(req, res) {
export const searchApps = async (req, res) => {
let page = Number(req.query.page) || 1;
let list_start = (page - 1) * 28;
@ -98,12 +92,10 @@ exports.searchApps = async function(req, res) {
apps_list += app_card;
}
// Render the home page
res.render("pages/apps", {
res.render("apps", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
isLoggedIn: true,
list_start: list_start + 1,
list_end: list_end,
app_count: templates.length,
@ -113,62 +105,3 @@ exports.searchApps = async function(req, res) {
});
}
exports.Install = async function (req, res) {
if (req.session.role == "admin") {
console.log(`Starting install for: ${req.body.name}`)
install(req.body);
let container_info = {
name: req.body.name,
service: req.body.service_name,
state: 'installing',
image: req.body.image,
restart_policy: req.body.restart_policy
}
let installCard = dashCard(container_info);
req.app.locals.install = installCard;
// Redirect to the home page
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
}
exports.Uninstall = async function (req, res) {
if (req.session.role == "admin") {
if (req.body.confirm == 'Yes') {
uninstall(req.body);
}
// Redirect to the home page
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
}

View file

@ -1,160 +0,0 @@
const User = require('../database/UserModel');
const bcrypt = require('bcrypt');
exports.Login = function(req,res){
// check whether we have a session
if(req.session.user){
// Redirect to log out.
res.redirect("/logout");
}else{
// Render the login page.
res.render("pages/login",{
"error":"",
"isLoggedIn": false
});
}
}
exports.processLogin = async function(req,res){
// get the data.
let email = req.body.email;
let password = req.body.password;
// check if we have data.
if(email && password){
// check if the user exists.
let existingUser = await User.findOne({ where: {email:email}});
if(existingUser){
// compare the password.
let match = await bcrypt.compare(password,existingUser.password);
if(match){
// set the session.
req.session.user = existingUser.username;
req.session.UUID = existingUser.UUID;
req.session.role = existingUser.role;
req.session.avatar = existingUser.avatar;
// Redirect to the home page.
res.redirect("/");
}else{
// return an error.
res.render("pages/login",{
"error":"Invalid password",
isLoggedIn: false
});
}
}else{
// return an error.
res.render("pages/login",{
"error":"User with that email does not exist.",
isLoggedIn:false
});
}
}else{
res.status(400);
res.render("pages/login",{
"error":"Please fill in all the fields.",
isLoggedIn:false
});
}
}
exports.Logout = function(req,res){
// clear the session.
req.session.destroy();
// Redirect to the login page.
res.redirect("/login");
}
exports.Register = function(req,res){
// Check whether we have a session
if(req.session.user){
// Redirect to log out.
res.redirect("/logout");
} else {
// Render the signup page.
res.render("pages/register",{
"error":"",
isLoggedIn:false
});
}
}
exports.processRegister = async function(req,res){
// Get the data.
let { first_name, last_name, username, email, password, avatar, tos, secret } = req.body;
let role = "user";
// Check the data.
if((first_name && last_name && email && password && username && tos) && (secret == process.env.SECRET)){
// Check if there is an existing user with that username.
let existingUser = await User.findOne({ where: {username:username}});
let adminUser = await User.findOne({ where: {role:"admin"}});
if(!existingUser){
// hash the password.
let hashedPassword = bcrypt.hashSync(password,10);
if(!adminUser){
console.log('Creating admin User');
role = "admin";
}
try {
const user = await User.create({
first_name: first_name,
last_name: last_name,
username: username,
email: email,
password: hashedPassword,
role: role,
group: 'all',
avatar: `<img src="./static/avatars/${avatar}">`
});
let newUser = await User.findOne({ where: {email:email}});
let match = await bcrypt.compare(password,newUser.password);
if(match){
console.log(`User session created for ${newUser.username}`)
req.session.user = newUser.username;
req.session.UUID = newUser.UUID;
req.session.role = newUser.role;
req.session.avatar = newUser.avatar;
}
// Redirect to the home page.
res.redirect("/");
}
catch (err) {
// return an error.
res.render("pages/register",{
"error":"Something went wrong when creating account.",
isLoggedIn:false
});
}
}else{
// return an error.
res.render("pages/register",{
"error":"User with that username already exists.",
isLoggedIn:false
});
}
}else{
// Redirect to the signup page.
res.render("pages/register",{
"error":"Please fill in all the fields and accept TOS.",
isLoggedIn:false
});
}
}

View file

@ -1,215 +1,24 @@
const { readFileSync, writeFileSync, appendFileSync, readdirSync } = require('fs');
const { execSync } = require("child_process");
const { siteCard } = require('../components/siteCard');
const { containerExec } = require('../functions/system')
export const Dashboard = (req, res) => {
exports.Dashboard = async function (req, res) {
let caddy = 'd-none';
if (process.env.Proxy_Manager == 'enabled') {
caddy = '';
}
// Render the home page
res.render("pages/dashboard", {
res.render("dashboard", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
isLoggedIn: true,
site_list: req.app.locals.site_list,
caddy: caddy
});
}
export const searchDashboard = (req, res) => {
console.log(req.params);
exports.AddSite = async function (req, res) {
let { domain, type, host, port } = req.body;
if ( domain && type && host && port) {
let { domain, type, host, port } = req.body;
// build caddyfile
let caddyfile = `${domain} {`
caddyfile += `\n\t${type} ${host}:${port}`
caddyfile += `\n\theader {`
caddyfile += `\n\t\tStrict-Transport-Security "max-age=31536000; includeSubDomains; preload"`
caddyfile += `\n\t}`
caddyfile += `\n}`
// save caddyfile
writeFileSync(`./caddyfiles/sites/${domain}.Caddyfile`, caddyfile, function (err) { console.log(err) });
// format caddyfile
let format = {
container: 'DweebProxy',
command: `caddy fmt --overwrite /etc/caddy/sites/${domain}.Caddyfile`
}
await containerExec(format, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Formatted ${domain}.Caddyfile`);
});
///////////////// convert caddyfile to json
let convert = {
container: 'DweebProxy',
command: `caddy adapt --config /etc/caddy/sites/${domain}.Caddyfile --pretty >> /etc/caddy/sites/${domain}.json`
}
await containerExec(convert, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Converted ${domain}.Caddyfile to JSON`);
});
////////////// reload caddy
let reload = {
container: 'DweebProxy',
command: `caddy reload --config /etc/caddy/Caddyfile`
}
await containerExec(reload, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Reloaded Caddy Config`);
});
let site = siteCard(type, domain, host, port, 0);
req.app.locals.site_list += site;
res.redirect("/");
} else {
// Redirect
console.log('missing info')
res.redirect("/");
}
}
exports.RemoveSite = async function (req, res) {
for (const [key, value] of Object.entries(req.body)) {
execSync(`rm ./caddyfiles/sites/${value}.Caddyfile`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`removed ${value}.Caddyfile`);
res.render("dashboard", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
});
}
let reload = {
container: 'DweebProxy',
command: `caddy reload --config /etc/caddy/Caddyfile`
}
await containerExec(reload);
console.log('Removed Site(s)')
res.redirect("/refreshsites");
}
exports.RefreshSites = async function (req, res) {
let domain, type, host, port;
let id = 1;
// Clear site_list.ejs
req.app.locals.site_list = "";
// check if ./caddyfiles/sites contains any .json files, then delete them
try {
let files = readdirSync('./caddyfiles/sites/');
files.forEach(file => {
if (file.includes(".json")) {
execSync(`rm ./caddyfiles/sites/${file}`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`removed ${file}`);
});
}
});
} catch (error) { console.log("No .json files to delete") }
// get list of Caddyfiles
let sites = readdirSync('./caddyfiles/sites/');
sites.forEach(site_name => {
// convert the caddyfile of each site to json
let convert = {
container: 'DweebProxy',
command: `caddy adapt --config ./caddyfiles/sites/${site_name} --pretty >> ./caddyfiles/sites/${site_name}.json`
}
containerExec(convert);
try {
// read the json file
let site_file = readFileSync(`./caddyfiles/sites/${site_name}.json`, 'utf8');
// fix whitespace and parse the json file
site_file = site_file.replace(/ /g, " ");
site_file = JSON.parse(site_file);
} catch (error) { console.log("No .json file to read") }
// get the domain, type, host, and port from the json file
try { domain = site_file.apps.http.servers.srv0.routes[0].match[0].host[0] } catch (error) { console.log("No Domain") }
try { type = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].handler } catch (error) { console.log("No Type") }
try { host = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].upstreams[0].dial.split(":")[0] } catch (error) { console.log("Not Localhost") }
try { port = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].upstreams[0].dial.split(":")[1] } catch (error) { console.log("No Port") }
// build the site card
let site = siteCard(type, domain, host, port, id);
// append the site card to site_list
req.app.locals.site_list += site;
id++;
});
res.redirect("/");
}
exports.DisableSite = async function (req, res) {
console.log(req.body)
console.log('Disable Site')
res.redirect("/");
}
exports.EnableSite = async function (req, res) {
console.log(req.body)
console.log('Enable Site')
res.redirect("/");
}

View file

@ -1,14 +1,18 @@
import { docker } from '../app.js';
export const Images = async function(req, res) {
exports.Images = async function(req, res) {
const allImages = await docker.listImages({ all: true });
for (let i = 0; i < allImages.length; i++) {
console.log(`Image ${i}:`)
console.log(`repoTags: ${allImages[i].repoTags}`)
}
// Render the home page
res.render("pages/images", {
res.render("images", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
isLoggedIn: true,
});
}

72
controllers/login.js Normal file
View file

@ -0,0 +1,72 @@
import { User } from '../database/models.js';
import { Syslog } from '../database/models.js';
import bcrypt from 'bcrypt';
export const Login = function(req,res){
if(req.session.user){
res.redirect("/logout");
}else{
res.render("login",{
"error":"",
});
}
}
export const submitLogin = async function(req,res){
let { email, password } = req.body;
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("/");
}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.",
});
}
}

View file

@ -1,14 +1,13 @@
const User = require('../database/UserModel');
exports.Networks = async function(req, res) {
import { docker } from '../app.js';
// Render the home page
res.render("pages/users", {
export const Networks = async function(req, res) {
res.render("networks", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
isLoggedIn: true,
});
}

96
controllers/register.js Normal file
View file

@ -0,0 +1,96 @@
import { User, Syslog } from '../database/models.js';
import bcrypt from 'bcrypt';
let SECRET = process.env.SECRET || "MrWiskers"
export const Register = function(req,res){
if(req.session.user){
res.redirect("/logout");
} else {
res.render("register",{
"error":"",
});
}
}
export const submitRegister = async function(req,res){
let { name, username, email, password, confirmPassword, avatar, tos, secret } = req.body;
if (secret != SECRET) {
const syslog = await Syslog.create({
user: username,
email: email,
event: "Failed Registration",
message: "Invalid secret",
ip: req.socket.remoteAddress
});
}
if((name && email && password && confirmPassword && username && tos) && (secret == SECRET) && (password == confirmPassword)){
async function userRole () {
let userCount = await User.count();
if(userCount == 0){
return "admin";
}else{
return "user";
}
}
let existingUser = await User.findOne({ where: {email:email}});
if(!existingUser){
try {
const user = await User.create({
name: name,
username: username,
email: email,
password: bcrypt.hashSync(password,10),
role: await userRole(),
group: 'all',
avatar: `<img src="img/avatars/${avatar}">`
});
// 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){
req.session.user = newUser.username;
req.session.UUID = newUser.UUID;
req.session.role = newUser.role;
req.session.avatar = newUser.avatar;
const syslog = await Syslog.create({
user: req.session.user,
email: email,
event: "Successful Registration",
message: "User registered successfully",
ip: req.socket.remoteAddress
});
res.redirect("/");
}
} catch(err) {
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.
res.render("register",{
"error":"Please fill in all the fields and accept TOS.",
});
}
}

View file

@ -1,14 +1,9 @@
const User = require('../database/UserModel.js');
const Server = require('../database/ServerModel.js');
exports.Settings = async function(req, res) {
export const Settings = (req, res) => {
// Render the home page
res.render("pages/settings", {
res.render("settings", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
isLoggedIn: true
});
}

36
controllers/syslogs.js Normal file
View file

@ -0,0 +1,36 @@
import { Syslog } from '../database/models.js';
export const Syslogs = async function(req, res) {
let logs = '';
const syslogs = await Syslog.findAll({
order: [
['id', 'DESC']
]
});
for (const log of syslogs) {
let date = (log.createdAt).toDateString();
let time = (log.createdAt).toLocaleTimeString();
let datetime = `${time} ${date}`;
logs += `<tr>
<td class="sort-id">${log.id}</td>
<td class="sort-user">${log.user}</td>
<td class="sort-email">${log.email}</td>
<td class="sort-event">${log.event}</td>
<td class="sort-message">${log.message}</td>
<td class="sort-ip">${log.ip}</td>
<td class="sort-datetime">${datetime}</td>
</tr>`
}
res.render("syslogs", {
name: req.session.user || 'Dev',
role: req.session.role || 'Dev',
avatar: req.session.avatar || '<img src="/img/avatars/rus.jpg">',
logs: logs
});
}

View file

@ -1,6 +1,6 @@
const User = require('../database/UserModel');
import { User } from '../database/models.js';
exports.Users = async function(req, res) {
export const Users = async (req, res) => {
let user_list = `
<tr>
@ -12,36 +12,48 @@ exports.Users = async function(req, res) {
<th>Email</th>
<th>UUID</th>
<th>Role</th>
<th>Last Login</th>
<th>Status</th>
<th>Actions</th>
</tr>`
let users = await User.findAll();
users.forEach((account) => {
full_name = account.first_name + ' ' + account.last_name;
user_info = `
let allUsers = await User.findAll();
allUsers.forEach((account) => {
let active = '<span class="badge badge-outline text-green">Active</span>'
let lastLogin = new Date(account.lastLogin);
let currentDate = new Date();
let days = Math.floor((currentDate - lastLogin) / (1000 * 60 * 60 * 24));
if (days > 30) {
active = '<span class="badge badge-outline text-grey">Inactive</span>';
}
let info = `
<tr>
<td><input class="form-check-input" type="checkbox"></td>
<td>${user.id}</td>
<td>${account.id}</td>
<td><span class="avatar me-2">${account.avatar}</span></td>
<td>${full_name}</td>
<td>${account.name}</td>
<td>${account.username}</td>
<td>${account.email}</td>
<td>${account.UUID}</td>
<td>${account.role}</td>
<td><span class="badge badge-outline text-green">Active</span></td>
<td>${account.lastLogin}</td>
<td>${active}</td>
<td><a href="#" class="btn">Edit</a></td>
</tr>`
user_list += user_info;
user_list += info;
});
// Render the home page
res.render("pages/users", {
res.render("users", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
isLoggedIn: true,
user_list: user_list
});

View file

@ -1,15 +1,9 @@
const User = require('../database/UserModel');
import { docker } from '../app.js';
exports.Volumes = async function(req, res) {
// Render the home page
res.render("pages/volumes", {
export const Volumes = (req, res) => {
res.render("volumes", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
isLoggedIn: true,
});
}

View file

@ -1,72 +0,0 @@
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database/db.sqlite',
logging: false
});
const Containers = sequelize.define('Containers', {
// Model attributes are defined here
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
visibility: {
type: DataTypes.STRING
// allowNull defaults to true
},
size: {
type: DataTypes.STRING
// allowNull defaults to true
},
group: {
type: DataTypes.STRING
// allowNull defaults to true
},
start: {
type: DataTypes.JSON
// allowNull defaults to true
},
stop: {
type: DataTypes.JSON
// allowNull defaults to true
},
pause: {
type: DataTypes.JSON
// allowNull defaults to true
},
restart: {
type: DataTypes.JSON
// allowNull defaults to true
},
remove: {
type: DataTypes.JSON
// allowNull defaults to true
},
logs: {
type: DataTypes.JSON
// allowNull defaults to true
},
update: {
type: DataTypes.JSON
// allowNull defaults to true
},
});
async function syncModel() {
await sequelize.sync();
console.log('Containers model synced');
}
syncModel();
module.exports = Containers;

View file

@ -1,46 +0,0 @@
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database/db.sqlite',
logging: false
});
const Server = sequelize.define('Server', {
// Model attributes are defined here
timezone: {
type: DataTypes.STRING,
allowNull: false
},
hwa: {
type: DataTypes.STRING
// allowNull defaults to true
},
media: {
type: DataTypes.STRING
// allowNull defaults to true
},
pgid: {
type: DataTypes.STRING
// allowNull defaults to true
},
puid: {
type: DataTypes.STRING
// allowNull defaults to true
},
caddy: {
type: DataTypes.STRING
// allowNull defaults to true
},
});
async function syncModel() {
await sequelize.sync();
console.log('Server model synced');
}
syncModel();
module.exports = Server;

View file

@ -1,63 +0,0 @@
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database/db.sqlite',
logging: false
});
const User = sequelize.define('User', {
// Model attributes are defined here
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
first_name: {
type: DataTypes.STRING,
allowNull: false
},
last_name: {
type: DataTypes.STRING
// allowNull defaults to true
},
username: {
type: DataTypes.STRING
// allowNull defaults to true
},
email: {
type: DataTypes.STRING
// allowNull defaults to true
},
password: {
type: DataTypes.STRING,
// allowNull: false
},
role: {
type: DataTypes.STRING
// allowNull defaults to true
},
group: {
type: DataTypes.STRING
// allowNull defaults to true
},
avatar: {
type: DataTypes.STRING
// allowNull defaults to true
},
UUID: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4
}
});
async function syncModel() {
await sequelize.sync();
console.log('User model synced');
}
syncModel();
module.exports = User;

162
database/models.js Normal file
View file

@ -0,0 +1,162 @@
import { Sequelize, DataTypes } from 'sequelize';
// let SQLITE_PASS = process.env.SQLITE_PASS || 'some_long_elaborate_password';
// export const sequelize = new Sequelize('dweebui', 'dweebui', SQLITE_PASS, {
// dialect: 'sqlite',
// dialectModulePath: '@journeyapps/sqlcipher',
// storage: './database/database.sqlite',
// logging: false,
// });
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database/database.sqlite',
logging: false,
});
export const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING
},
username: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false
},
password: {
type: DataTypes.STRING,
allowNull: false
},
role: {
type: DataTypes.STRING
},
group: {
type: DataTypes.STRING
},
avatar: {
type: DataTypes.STRING
},
lastLogin: {
type: DataTypes.STRING
},
UUID: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
}
});
export const Container = sequelize.define('Container', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
visibility: {
type: DataTypes.STRING
},
size: {
type: DataTypes.STRING
},
group: {
type: DataTypes.STRING
}
});
export const Permission = sequelize.define('Permission', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
containerName: {
type: DataTypes.STRING,
allowNull: false
},
containerID: {
type: DataTypes.STRING,
allowNull: false
},
user: {
type: DataTypes.STRING,
allowNull: false
},
userID: {
type: DataTypes.STRING,
allowNull: false
},
install: {
type: DataTypes.STRING,
},
uninstall: {
type: DataTypes.STRING
},
edit: {
type: DataTypes.STRING
},
upgrade: {
type: DataTypes.STRING
},
start: {
type: DataTypes.STRING
},
stop: {
type: DataTypes.STRING
},
restart: {
type: DataTypes.STRING
},
pause: {
type: DataTypes.STRING
},
logs: {
type: DataTypes.STRING
},
hide: {
type: DataTypes.STRING
},
view: {
type: DataTypes.STRING
},
reset_view: {
type: DataTypes.STRING
},
});
export const Syslog = sequelize.define('Syslog', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
user: {
type: DataTypes.STRING
},
email: {
type: DataTypes.STRING
},
event: {
type: DataTypes.STRING,
allowNull: false
},
message: {
type: DataTypes.STRING,
allowNull: false
},
ip : {
type: DataTypes.STRING
},
});

View file

@ -2,9 +2,9 @@ version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.09-dev
# build:
# context: .
# image: lllllllillllllillll/dweebui:v0.20
build:
context: .
environment:
NODE_ENV: production
PORT: 8000
@ -14,17 +14,13 @@ services:
- 8000:8000
volumes:
- dweebui:/app
- caddyfiles:/app/caddyfiles
- /var/run/docker.sock:/var/run/docker.sock
#- ./custom-templates.json:/app/custom-templates.json
#- ./composefiles:/app/composefiles
networks:
- dweeb_network
- dweebui_net
volumes:
dweebui:
caddyfiles:
networks:
dweeb_network:
dweebui_net:
driver: bridge

View file

@ -1,205 +0,0 @@
const { writeFileSync, mkdirSync, readFileSync } = require("fs");
const yaml = require('js-yaml');
const { exec, execSync } = require("child_process");
const { docker } = require('./system');
var DockerodeCompose = require('dockerode-compose');
module.exports.install = async function (data) {
console.log(`[Start of install function]`);
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
let { port0, port1, port2, port3, port4, port5 } = data;
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
if ((service_name.includes('caddy')) || (name.includes('caddy'))) {
req.app.locals.caddy = 'enabled';
}
let docker_volumes = [];
if (image.startsWith('https://')){
mkdirSync(`./appdata/${name}`, { recursive: true });
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
console.log(`Downloaded stackfile: ${image}`);
let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
let services = Object.keys(stackfile.services);
for ( let i = 0; i < services.length; i++ ) {
try {
console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
} catch { console.log('no env') }
}
} else {
let compose_file = `version: '3'`;
compose_file += `\nservices:`
compose_file += `\n ${service_name}:`
compose_file += `\n container_name: ${name}`;
compose_file += `\n image: ${image}`;
// Command
if (command_check == 'on') {
compose_file += `\n command: ${command}`
}
// Network mode
if (net_mode == 'host') {
compose_file += `\n network_mode: 'host'`
}
else if (net_mode != 'host' && net_mode != 'docker') {
compose_file += `\n network_mode: '${net_mode}'`
}
// Restart policy
if (restart_policy != '') {
compose_file += `\n restart: ${restart_policy}`
}
// Ports
if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
compose_file += `\n ports:`
for (let i = 0; i < 6; i++) {
if (data[`port${i}`] == 'on') {
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
}
}
}
// Volumes
if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
compose_file += `\n volumes:`
for (let i = 0; i < 6; i++) {
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
}
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
docker_volumes.push(`${name}_${volume_name}`);
}
}
}
// Environment variables
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
compose_file += `\n environment:`
}
for (let i = 0; i < 12; i++) {
if (data[`env${i}`] == 'on') {
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
}
}
// Add labels
if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
compose_file += `\n labels:`
}
for (let i = 0; i < 12; i++) {
if (data[`label${i}`] == 'on') {
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
}
}
// Add privileged mode
if (data.privileged == 'on') {
compose_file += `\n privileged: true`
}
// Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
for (let i = 0; i < 12; i++) {
if (data[`env${i}`] == 'on') {
if (data[`env_${i}_name`] == 'DRINODE') {
compose_file += `\n deploy:`
compose_file += `\n resources:`
compose_file += `\n reservations:`
compose_file += `\n devices:`
compose_file += `\n - driver: nvidia`
compose_file += `\n count: 1`
compose_file += `\n capabilities: [gpu]`
}
}
}
}
// add any docker volumes to the docker-compose file
if ( docker_volumes.length > 0 ) {
compose_file += `\n`
compose_file += `\nvolumes:`
// check docker_volumes for duplicates and remove them completely
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
for (let i = 0; i < docker_volumes.length; i++) {
if ( docker_volumes[i] != '') {
compose_file += `\n ${docker_volumes[i]}:`
}
}
}
try {
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
} catch { console.log('error creating directory or compose file') }
try {
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
(async () => {
await compose.pull();
await compose.up();
})();
} catch { console.log('error running compose file')}
}
}
module.exports.uninstall = async function (data) {
if (data.confirm == 'Yes') {
var containerName = docker.getContainer(`${data.service_name}`);
try {
containerName.stop(function (err, data) {
if (data) {
containerName.remove(function (err, data) {
});
}
});
} catch {
containerName.remove(function (err, data) {
});
}
}
}

View file

@ -1,194 +0,0 @@
const { writeFileSync, mkdirSync, readFileSync } = require("fs");
const yaml = require('js-yaml');
const { execSync } = require("child_process");
const { docker } = require('./system');
var DockerodeCompose = require('dockerode-compose');
module.exports.install = async function (data) {
console.log(`[Start of install function]`);
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
let { port0, port1, port2, port3, port4, port5 } = data;
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
let docker_volumes = [];
if (image.startsWith('https://')){
mkdirSync(`./appdata/${name}`, { recursive: true });
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
console.log(`Downloaded stackfile: ${image}`);
let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
let services = Object.keys(stackfile.services);
for ( let i = 0; i < services.length; i++ ) {
try {
console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
} catch { console.log('no env') }
}
} else {
let compose_file = `version: '3'`;
compose_file += `\nservices:`
compose_file += `\n ${service_name}:`
compose_file += `\n container_name: ${name}`;
compose_file += `\n image: ${image}`;
// Command
if (command_check == 'on') {
compose_file += `\n command: ${command}`
}
// Network mode
if (net_mode == 'host') {
compose_file += `\n network_mode: 'host'`
}
else if (net_mode != 'host' && net_mode != 'docker') {
compose_file += `\n network_mode: '${net_mode}'`
}
// Restart policy
if (restart_policy != '') {
compose_file += `\n restart: ${restart_policy}`
}
// Ports
if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
compose_file += `\n ports:`
for (let i = 0; i < 6; i++) {
if (data[`port${i}`] == 'on') {
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
}
}
}
// Volumes
if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
compose_file += `\n volumes:`
for (let i = 0; i < 6; i++) {
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
}
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
docker_volumes.push(`${name}_${volume_name}`);
}
}
}
// Environment variables
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
compose_file += `\n environment:`
}
for (let i = 0; i < 12; i++) {
if (data[`env${i}`] == 'on') {
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
}
}
// Add labels
if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
compose_file += `\n labels:`
}
for (let i = 0; i < 12; i++) {
if (data[`label${i}`] == 'on') {
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
}
}
// Add privileged mode
if (data.privileged == 'on') {
compose_file += `\n privileged: true`
}
// Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
for (let i = 0; i < 12; i++) {
if (data[`env${i}`] == 'on') {
if (data[`env_${i}_name`] == 'DRINODE') {
compose_file += `\n deploy:`
compose_file += `\n resources:`
compose_file += `\n reservations:`
compose_file += `\n devices:`
compose_file += `\n - driver: nvidia`
compose_file += `\n count: 1`
compose_file += `\n capabilities: [gpu]`
}
}
}
}
// add any docker volumes to the docker-compose file
if ( docker_volumes.length > 0 ) {
compose_file += `\n`
compose_file += `\nvolumes:`
// check docker_volumes for duplicates and remove them completely
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
for (let i = 0; i < docker_volumes.length; i++) {
if ( docker_volumes[i] != '') {
compose_file += `\n ${docker_volumes[i]}:`
}
}
}
try {
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
} catch { console.log('error creating directory or compose file') }
try {
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
(async () => {
await compose.pull();
await compose.up();
})();
} catch { console.log('error running compose file')}
}
}
module.exports.uninstall = async function (data) {
if (data.confirm == 'Yes') {
console.log(`Uninstalling ${data.service_name}: ${data}`);
var containerName = docker.getContainer(`${data.service_name}`);
try {
await containerName.stop();
console.log(`Stopped ${data.service_name} container`);
} catch {
console.log(`Error stopping ${data.service_name} container`);
}
try {
await containerName.remove();
console.log(`Removed ${data.service_name} container`);
} catch {
console.log(`Error removing ${data.service_name} container`);
}
}
}

View file

@ -1,382 +0,0 @@
const { currentLoad, mem, networkStats, fsSize, dockerContainerStats, dockerImages, networkInterfaces, dockerInfo } = require('systeminformation');
var Docker = require('dockerode');
var docker = new Docker({ socketPath: '/var/run/docker.sock' });
const { dashCard } = require('../components/dashCard');
const { Readable } = require('stream');
const Containers = require('../database/ContainerModel');
// export docker
module.exports.docker = docker;
let IPv4 = '';
networkInterfaces().then(data => {
IPv4 = data[0].ip4;
});
let hidden = '';
module.exports.hiddenContainers = async function () {
hidden = await Containers.findAll({ where: {visibility:false}});
hidden = hidden.map(a => a.name);
}
module.exports.serverStats = async function () {
const cpuUsage = await currentLoad();
const ramUsage = await mem();
const netUsage = await networkStats();
const diskUsage = await fsSize();
const info = {
cpu: Math.round(cpuUsage.currentLoad),
ram: Math.round((ramUsage.active / ramUsage.total) * 100),
tx: netUsage[0].tx_bytes,
rx: netUsage[0].rx_bytes,
disk: diskUsage[0].use,
};
return info;
}
module.exports.containerList = async function () {
let card_list = '';
const data = await docker.listContainers({ all: true });
for (const container of data) {
if (!hidden.includes(container.Names[0].slice(1))) {
let imageVersion = container.Image.split('/');
let service = imageVersion[imageVersion.length - 1].split(':')[0];
let containerId = docker.getContainer(container.Id);
let containerInfo = await containerId.inspect();
// Get ports //////////////////////////
let ports_list = [];
try {
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
let ports = {
check : 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {
// console.log('no ports')
}
for (let i = 0; i < 12; i++) {
if (ports_list[i] == undefined) {
let ports = {
check : '',
external: '',
internal: '',
protocol: ''
}
ports_list[i] = ports;
}
} /////////////////////////////////////
// Get volumes ////////////////////////
let volumes_list = [];
try { for (const [key, value] of Object.entries(containerInfo.HostConfig.Binds)) {
let volumes = {
check : 'checked',
bind: value.split(':')[0],
container: value.split(':')[1],
readwrite: value.split(':')[2]
}
volumes_list.push(volumes);
}} catch {
// console.log('no volumes')
}
for (let i = 0; i < 12; i++) {
if (volumes_list[i] == undefined) {
let volumes = {
check : '',
bind: '',
container: '',
readwrite: ''
}
volumes_list[i] = volumes;
}
} /////////////////////////////////////
// Get environment variables.
let environment_variables = [];
try { for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
let env = {
check : 'checked',
name: value.split('=')[0],
default: value.split('=')[1]
}
environment_variables.push(env);
}} catch { console.log('no env') }
for (let i = 0; i < 12; i++) {
if (environment_variables[i] == undefined) {
let env = {
check : '',
name: '',
default: ''
}
environment_variables[i] = env;
}
}
// Get labels.
let labels = [];
for (const [key, value] of Object.entries(containerInfo.Config.Labels)) {
let label = {
check : 'checked',
name: key,
value: value
}
labels.push(label);
}
for (let i = 0; i < 12; i++) {
if (labels[i] == undefined) {
let label = {
check : '',
name: '',
value: ''
}
labels[i] = label;
}
}
let container_info = {
name: container.Names[0].slice(1),
service: service,
id: container.Id,
state: container.State,
image: container.Image,
external_port: ports_list[0].external || 0,
internal_port: ports_list[0].internal || 0,
ports: ports_list,
volumes: volumes_list,
environment_variables: environment_variables,
labels: labels,
IPv4: IPv4,
style: "Compact"
}
let dockerCard = dashCard(container_info);
card_list += dockerCard;
}
}
return card_list;
}
module.exports.containerStats = async function () {
let container_stats = [];
const data = await docker.listContainers({ all: true });
for (const container of data) {
if (!hidden.includes(container.Names[0].slice(1))) {
const stats = await dockerContainerStats(container.Id);
let container_stat = {
name: container.Names[0].slice(1),
cpu: Math.round(stats[0].cpuPercent),
ram: Math.round(stats[0].memPercent)
}
//push stats to an array
container_stats.push(container_stat);
}
}
return container_stats;
}
module.exports.containerAction = async function (data) {
let { user, role, action, container, state } = data;
console.log(`${user} wants to: ${action} ${container}`);
if (role == 'admin') {
var containerName = docker.getContainer(container);
if ((action == 'start') && (state == 'stopped')) {
containerName.start();
} else if ((action == 'start') && (state == 'paused')) {
containerName.unpause();
} else if ((action == 'stop') && (state != 'stopped')) {
containerName.stop();
} else if ((action == 'pause') && (state == 'running')) {
containerName.pause();
} else if ((action == 'pause') && (state == 'paused')) {
containerName.unpause();
} else if (action == 'restart') {
containerName.restart();
}
} else {
console.log('User is not an admin');
}
}
module.exports.containerExec = async function (data) {
let { container, command } = data;
var containerName = docker.getContainer(container);
var options = {
Cmd: ['/bin/sh', '-c', command],
AttachStdout: true,
AttachStderr: true,
Tty: true
};
containerName.exec(options, function (err, exec) {
if (err) return;
exec.start(function (err, stream) {
if (err) return;
containerName.modem.demuxStream(stream, process.stdout, process.stderr);
exec.inspect(function (err, data) {
if (err) return;
});
});
});
}
module.exports.containerLogs = function (data) {
return new Promise((resolve, reject) => {
let logString = '';
var options = {
follow: false,
stdout: true,
stderr: false,
timestamps: false
};
var containerName = docker.getContainer(data);
containerName.logs(options, function (err, stream) {
if (err) {
reject(err);
return;
}
const readableStream = Readable.from(stream);
readableStream.on('data', function (chunk) {
logString += chunk.toString('utf8');
});
readableStream.on('end', function () {
resolve(logString);
});
});
});
};
module.exports.dockerImages = async function () {
// get the names, tags, status, created date, and size of all images
const data1 = await dockerImages({ all: true });
const data2 = await docker.listImages({ all: true });
// for ( i = 0; i < data.length; i++) {
// console.log(`Image ${i}:`)
// console.log(`repoTags: ${data[i].repoTags}`)
// }
// console.log(data1);
console.log(data2);
}
module.exports.dockerVolumes = async function () {
let volume_list = '';
const data = await docker.listVolumes();
return data;
// for (const volume of data.Volumes) {
// let volume_info = {
// name: volume.Name,
// style: "Compact"
// }
// let dockerCard = dashCard(volume_info);
// volume_list += dockerCard;
// }
// return volume_list;
}
module.exports.dockerNetworks = async function () {
let network_list = '';
const data = await docker.listNetworks();
return data;
// for (const network of data) {
// let network_info = {
// name: network.Name,
// style: "Compact"
// }
// let dockerCard = dashCard(network_info);
// network_list += dockerCard;
// }
// return network_list;
}

2355
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,25 +1,35 @@
{
"name": "dweebui",
"version": "1.0.0",
"description": "A web UI for Docker",
"main": "app.js",
"type": "module",
"scripts": {
"test": "mocha --require @babel/register --exit"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.0",
"child_process": "^1.0.2",
"@babel/register": "^7.23.7",
"@journeyapps/sqlcipher": "^5.3.1",
"@socket.io/admin-ui": "^0.5.1",
"bcrypt": "^5.1.1",
"chai": "^5.0.0",
"compression": "^1.7.4",
"dockerode": "^4.0.0",
"dockerode-compose": "^1.4.0",
"cors": "^2.8.5",
"dockerode": "^4.0.1",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-session": "^1.17.3",
"helmet": "^7.1.0",
"js-yaml": "^4.1.0",
"mocha": "^10.2.0",
"sequelize": "^6.35.2",
"socket.io": "^4.6.1",
"sinon": "^17.0.1",
"socket.io": "^4.7.2",
"sqlite3": "^5.1.6",
"systeminformation": "^5.21.20"
},
"description": ""
"stream": "^0.0.2",
"supertest": "^6.3.3",
"systeminformation": "^5.21.22"
}
}

View file

@ -2,7 +2,7 @@
.meter {
box-sizing: content-box;
height: 15px; /* Can be anything */
height: 15px;
margin-left: auto;
margin-right: auto;
position: relative;

View file

@ -11462,47 +11462,38 @@ fieldset:disabled .btn {
}
.column-gap-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -12910,17 +12901,14 @@ fieldset:disabled .btn {
}
.columns-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
@ -13821,47 +13809,38 @@ fieldset:disabled .btn {
}
.column-gap-sm-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-sm-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-sm-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-sm-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-sm-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-sm-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-sm-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-sm-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-sm-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -13878,17 +13857,14 @@ fieldset:disabled .btn {
}
.columns-sm-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-sm-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-sm-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
}
@ -14790,47 +14766,38 @@ fieldset:disabled .btn {
}
.column-gap-md-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-md-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-md-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-md-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-md-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-md-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-md-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-md-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-md-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -14847,17 +14814,14 @@ fieldset:disabled .btn {
}
.columns-md-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-md-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-md-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
}
@ -15759,47 +15723,38 @@ fieldset:disabled .btn {
}
.column-gap-lg-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-lg-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-lg-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-lg-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-lg-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-lg-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-lg-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-lg-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-lg-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -15816,17 +15771,14 @@ fieldset:disabled .btn {
}
.columns-lg-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-lg-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-lg-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
}
@ -16728,47 +16680,38 @@ fieldset:disabled .btn {
}
.column-gap-xl-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-xl-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-xl-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-xl-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-xl-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-xl-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-xl-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-xl-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-xl-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -16785,17 +16728,14 @@ fieldset:disabled .btn {
}
.columns-xl-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-xl-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-xl-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
}
@ -17697,47 +17637,38 @@ fieldset:disabled .btn {
}
.column-gap-xxl-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-xxl-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-xxl-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-xxl-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-xxl-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-xxl-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-xxl-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-xxl-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-xxl-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -17754,17 +17685,14 @@ fieldset:disabled .btn {
}
.columns-xxl-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-xxl-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-xxl-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

59
public/fonts/inter.css Normal file
View file

@ -0,0 +1,59 @@
/* Variable fonts usage:
:root { font-family: "Inter", sans-serif; }
@supports (font-variation-settings: normal) {
:root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; }
} */
@font-face {
font-family: InterVariable;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('InterVariable.woff2?v=4.0') format('woff2');
}
@font-face {
font-family: InterVariable;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url('InterVariable-Italic.woff2?v=4.0') format('woff2');
}
/* legacy name "Inter var" (Oct 2023) */
@font-face { font-family:'Inter var'; font-style:normal; font-weight:100 900; font-display:swap; src: url('InterVariable.woff2?v=4.0') format('woff2'); }
@font-face { font-family:'Inter var'; font-style:italic; font-weight:100 900; font-display:swap; src: url('InterVariable-Italic.woff2?v=4.0') format('woff2'); }
/* static fonts */
@font-face { font-family:Inter; font-style:normal; font-weight:100; font-display:swap; src:url("Inter-Thin.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:100; font-display:swap; src:url("Inter-ThinItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:200; font-display:swap; src:url("Inter-ExtraLight.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:200; font-display:swap; src:url("Inter-ExtraLightItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:300; font-display:swap; src:url("Inter-Light.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:300; font-display:swap; src:url("Inter-LightItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:400; font-display:swap; src:url("Inter-Regular.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:400; font-display:swap; src:url("Inter-Italic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:500; font-display:swap; src:url("Inter-Medium.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:500; font-display:swap; src:url("Inter-MediumItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:600; font-display:swap; src:url("Inter-SemiBold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:600; font-display:swap; src:url("Inter-SemiBoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:700; font-display:swap; src:url("Inter-Bold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:700; font-display:swap; src:url("Inter-BoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:800; font-display:swap; src:url("Inter-ExtraBold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:800; font-display:swap; src:url("Inter-ExtraBoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:900; font-display:swap; src:url("Inter-Black.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:900; font-display:swap; src:url("Inter-BlackItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:100; font-display:swap; src:url("InterDisplay-Thin.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:100; font-display:swap; src:url("InterDisplay-ThinItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:200; font-display:swap; src:url("InterDisplay-ExtraLight.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:200; font-display:swap; src:url("InterDisplay-ExtraLightItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:300; font-display:swap; src:url("InterDisplay-Light.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:300; font-display:swap; src:url("InterDisplay-LightItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:400; font-display:swap; src:url("InterDisplay-Regular.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:400; font-display:swap; src:url("InterDisplay-Italic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:500; font-display:swap; src:url("InterDisplay-Medium.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:500; font-display:swap; src:url("InterDisplay-MediumItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:600; font-display:swap; src:url("InterDisplay-SemiBold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:600; font-display:swap; src:url("InterDisplay-SemiBoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:700; font-display:swap; src:url("InterDisplay-Bold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:700; font-display:swap; src:url("InterDisplay-BoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:800; font-display:swap; src:url("InterDisplay-ExtraBold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:800; font-display:swap; src:url("InterDisplay-ExtraBoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:900; font-display:swap; src:url("InterDisplay-Black.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:900; font-display:swap; src:url("InterDisplay-BlackItalic.woff2?v=4.0") format("woff2"); }

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -12,7 +12,7 @@
})((function () { 'use strict';
var themeStorageKey = "tablerTheme";
var defaultTheme = "light";
var defaultTheme = "dark";
var selectedTheme;
var params = new Proxy(new URLSearchParams(window.location.search), {
get: function get(searchParams, prop) {

View file

@ -1,16 +1,6 @@
// SOCKET IO
const socket = io({
auth: {
token: "abc"
}
});
socket.on('connect', () => { console.log('connected'); });
// ON CONNECT EVENT
socket.on('connect', () => {
console.log('Connected');
});
// SELECT METRICS ELEMENTS
// Server metrics
const cpuText = document.getElementById('cpu-text');
const cpuBar = document.getElementById('cpu-bar');
const ramText = document.getElementById('ram-text');
@ -20,19 +10,22 @@ const netBar = document.getElementById('net-bar');
const diskText = document.getElementById('disk-text');
const diskBar = document.getElementById('disk-bar');
// Container cards
const dockerCards = document.getElementById('cards');
// Container logs
const logViewer = document.getElementById('logView');
//Update usage bars
// Server metrics
socket.on('metrics', (data) => {
let {cpu, ram, tx, rx, disk} = data;
let [cpu, ram, tx, rx, disk] = data;
cpuText.innerHTML = `<span>CPU ${cpu} %</span>`;
if (cpu < 7 ) { cpu = 7; }
cpuBar.innerHTML = `<span style="width: ${cpu}%"><span></span></span>`;
ramText.innerHTML = `<span>RAM ${ram} %</span>`;
if (ram < 7 ) { ram = 7; }
ramBar.innerHTML = `<span style="width: ${ram}%"><span></span></span>`;
tx = Math.round(tx / 1024 / 1024);
@ -42,18 +35,26 @@ socket.on('metrics', (data) => {
netBar.innerHTML = `<span style="width: 50%"><span></span></span>`;
diskText.innerHTML = `<span>DISK ${disk} %</span>`;
if (disk < 7 ) { disk = 7; }
diskBar.innerHTML = `<span style="width: ${disk}%"><span></span></span>`;
});
function drawCharts(name, cpu_array, ram_array) {
var elements = document.querySelectorAll(`${name}`);
// Container cards
socket.on('containers', (data) => {
let deleteMeElements = document.querySelectorAll('.deleteme');
deleteMeElements.forEach((element) => {
element.parentNode.removeChild(element);
});
dockerCards.insertAdjacentHTML("afterend", data);
});
Array.from(elements).forEach(function(element) {
if (window.ApexCharts) {
new ApexCharts(element, {
function drawCharts(name, cpuArray, ramArray) {
let element = document.querySelector(`${name}`);
let chart = new ApexCharts(element, {
chart: {
type: "line",
fontFamily: 'inherit',
height: 40.0,
sparkline: {
enabled: true
@ -73,13 +74,13 @@ function drawCharts(name, cpu_array, ram_array) {
},
series: [{
name: "CPU",
data: cpu_array
data: cpuArray
}, {
name: "RAM",
data: ram_array
data: ramArray
}],
tooltip: {
theme: 'dark'
enabled: false
},
grid: {
strokeDashArray: 4
@ -90,117 +91,61 @@ function drawCharts(name, cpu_array, ram_array) {
},
tooltip: {
enabled: false
},
type: 'datetime'
}
},
yaxis: {
labels: {
padding: 4
}
},
labels: [
'2020-06-20', '2020-06-21', '2020-06-22', '2020-06-23', '2020-06-24', '2020-06-25', '2020-06-26', '2020-06-27', '2020-06-28', '2020-06-29', '2020-06-30', '2020-07-01', '2020-07-02', '2020-07-03', '2020-07-04', '2020-07-05', '2020-07-06', '2020-07-07', '2020-07-08', '2020-07-09', '2020-07-10', '2020-07-11', '2020-07-12', '2020-07-13', '2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17', '2020-07-18', '2020-07-19'
],
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
legend: {
show: false
}
}).render();
}
});
})
chart.render();
}
// container button actions
function buttonAction(button) {
socket.emit('clicked', {container: button.name, state: button.id, action: button.value});
// Buttons functions
function clicked(button) {
socket.emit('clicked', {name: button.name, id: button.id, value: button.value});
}
function hideContainer(button) {
socket.emit('hide', {container: button.name});
}
function resetView() {
socket.emit('reset');
}
let containerLogs;
function viewLogs(button) {
if (button.name != 'refresh') {
containerLogs = button.name;
}
socket.emit('logs', {container: containerLogs});
}
socket.on('logString', (data) => {
logViewer.innerHTML = `<pre>${data}</pre>`;
});
socket.on('cards', (data) => {
console.log('cards deleted');
let deleteMeElements = document.querySelectorAll('.deleteme');
deleteMeElements.forEach((element) => {
element.parentNode.removeChild(element);
});
dockerCards.insertAdjacentHTML("afterend", data);
// check localStorage for items ending with _cpu and redraw the charts
for (let i = 0; i < localStorage.length; i++) {
if (localStorage.key(i).endsWith('_cpu')) {
let name = localStorage.key(i).split('_')[0];
let cpu_array = JSON.parse(localStorage.getItem(`${name}_cpu`));
let ram_array = JSON.parse(localStorage.getItem(`${name}_ram`));
drawCharts(`#${name}_chart`, cpu_array, ram_array);
}
}
});
socket.on('containerStats', (data) => {
let containerStats = data;
let {name, cpu, ram} = data;
for (const [name, statsArray] of Object.entries(containerStats)) {
console.log(`drawing chart for ${name}`)
let cpuArray = statsArray.cpuArray;
let ramArray = statsArray.ramArray;
var cpu_array = JSON.parse(localStorage.getItem(`${name}_cpu`));
var ram_array = JSON.parse(localStorage.getItem(`${name}_ram`));
if (cpu_array == null) { cpu_array = Array(30).fill(0); }
if (ram_array == null) { ram_array = Array(30).fill(0); }
cpu_array.push(cpu);
ram_array.push(ram);
cpu_array = cpu_array.slice(-30);
ram_array = ram_array.slice(-30);
localStorage.setItem(`${name}_cpu`, JSON.stringify(cpu_array));
localStorage.setItem(`${name}_ram`, JSON.stringify(ram_array));
// replace the old chart with the new one
let chart = document.getElementById(`${name}_chart`);
if (chart) {
let newChart = document.createElement('div');
newChart.id = `${name}_chart`;
chart.parentNode.replaceChild(newChart, chart);
drawCharts(`#${name}_chart`, cpu_array, ram_array);
chart.innerHTML = '';
drawCharts(`#${name}_chart`, cpuArray, ramArray);
} else {
console.log(`Chart element with id ${name}_chart not found in the DOM`);
}
}
});
socket.on('install', (data) => {
console.log('added install card');
dockerCards.insertAdjacentHTML("afterend", data);
// socket.on('install', (data) => {
// dockerCards.insertAdjacentHTML("afterend", data);
// });
// let containerLogs;
// function viewLogs(button) {
// if (button.name != 'refresh') {
// containerLogs = button.name;
// }
// socket.emit('logs', {container: containerLogs});
// }
socket.on('logs', (data) => {
logViewer.innerHTML = `<pre>${data}</pre>`;
});

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 68">
<path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 599 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232 68">
<path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#fff"/>
<path d="M105.8 46.1c.4 0 .9.2 1.2.6s.6 1 .6 1.7c0 .9-.5 1.6-1.4 2.2s-2 .9-3.2.9c-2 0-3.7-.4-5-1.3s-2-2.6-2-5.4V31.6h-2.2c-.8 0-1.4-.3-1.9-.8s-.9-1.1-.9-1.9c0-.7.3-1.4.8-1.8s1.2-.7 1.9-.7h2.2v-3.1c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v3.1h3.4c.8 0 1.4.3 1.9.8s.8 1.2.8 1.9-.3 1.4-.8 1.8-1.2.7-1.9.7h-3.4v13c0 .7.2 1.2.5 1.5s.8.5 1.4.5c.3 0 .6-.1 1.1-.2.5-.2.8-.3 1.2-.3zm28-20.7c.8 0 1.5.3 2.1.8.5.5.8 1.2.8 2.1v20.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2-.8-.8-1.2-.8-2.1c-.8.9-1.9 1.7-3.2 2.4-1.3.7-2.8 1-4.3 1-2.2 0-4.2-.6-6-1.7-1.8-1.1-3.2-2.7-4.2-4.7s-1.6-4.3-1.6-6.9c0-2.6.5-4.9 1.5-6.9s2.4-3.6 4.2-4.8c1.8-1.1 3.7-1.7 5.9-1.7 1.5 0 3 .3 4.3.8 1.3.6 2.5 1.3 3.4 2.1 0-.8.3-1.5.8-2.1.5-.5 1.2-.7 2-.7zm-9.7 21.3c2.1 0 3.8-.8 5.1-2.3s2-3.4 2-5.7-.7-4.2-2-5.8c-1.3-1.5-3-2.3-5.1-2.3-2 0-3.7.8-5 2.3-1.3 1.5-2 3.5-2 5.8s.6 4.2 1.9 5.7 3 2.3 5.1 2.3zm32.1-21.3c2.2 0 4.2.6 6 1.7 1.8 1.1 3.2 2.7 4.2 4.7s1.6 4.3 1.6 6.9-.5 4.9-1.5 6.9-2.4 3.6-4.2 4.8c-1.8 1.1-3.7 1.7-5.9 1.7-1.5 0-3-.3-4.3-.9s-2.5-1.4-3.4-2.3v.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.5-.8-1.2-.8-2.1V18.9c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v10c.8-1 1.8-1.8 3.2-2.5 1.3-.7 2.8-1 4.3-1zm-.7 21.3c2 0 3.7-.8 5-2.3s2-3.5 2-5.8-.6-4.2-1.9-5.7-3-2.3-5.1-2.3-3.8.8-5.1 2.3-2 3.4-2 5.7.7 4.2 2 5.8c1.3 1.6 3 2.3 5.1 2.3zm23.6 1.9c0 .8-.3 1.5-.8 2.1s-1.3.8-2.1.8-1.5-.3-2-.8-.8-1.3-.8-2.1V18.9c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v29.7zm29.3-10.5c0 .8-.3 1.4-.9 1.9-.6.5-1.2.7-2 .7h-15.8c.4 1.9 1.3 3.4 2.6 4.4 1.4 1.1 2.9 1.6 4.7 1.6 1.3 0 2.3-.1 3.1-.4.7-.2 1.3-.5 1.8-.8.4-.3.7-.5.9-.6.6-.3 1.1-.4 1.6-.4.7 0 1.2.2 1.7.7s.7 1 .7 1.7c0 .9-.4 1.6-1.3 2.4-.9.7-2.1 1.4-3.6 1.9s-3 .8-4.6.8c-2.7 0-5-.6-7-1.7s-3.5-2.7-4.6-4.6-1.6-4.2-1.6-6.6c0-2.8.6-5.2 1.7-7.2s2.7-3.7 4.6-4.8 3.9-1.7 6-1.7 4.1.6 6 1.7 3.4 2.7 4.5 4.7c.9 1.9 1.5 4.1 1.5 6.3zm-12.2-7.5c-3.7 0-5.9 1.7-6.6 5.2h12.6v-.3c-.1-1.3-.8-2.5-2-3.5s-2.5-1.4-4-1.4zm30.3-5.2c1 0 1.8.3 2.4.8.7.5 1 1.2 1 1.9 0 1-.3 1.7-.8 2.2-.5.5-1.1.8-1.8.7-.5 0-1-.1-1.6-.3-.2-.1-.4-.1-.6-.2-.4-.1-.7-.1-1.1-.1-.8 0-1.6.3-2.4.8s-1.4 1.3-1.9 2.3-.7 2.3-.7 3.7v11.4c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.6-.8-1.3-.8-2.1V28.8c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v.6c.7-1.3 1.8-2.3 3.2-3 1.3-.7 2.8-1 4.3-1z" fill-rule="evenodd" clip-rule="evenodd" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

60
router/index.js Normal file
View file

@ -0,0 +1,60 @@
import express from "express";
import { io } from "../app.js";
import { Login, submitLogin } from "../controllers/login.js";
import { Register, submitRegister } from "../controllers/register.js";
import { Dashboard, searchDashboard } from "../controllers/dashboard.js";
import { Apps } from "../controllers/apps.js";
import { Users } from "../controllers/users.js";
import { Images } from "../controllers/images.js";
import { Account } from "../controllers/account.js";
import { Settings } from "../controllers/settings.js";
import { Networks } from "../controllers/networks.js";
import { Volumes } from "../controllers/volumes.js";
import { Syslogs } from "../controllers/syslogs.js";
export const router = express.Router();
const auth = (req, res, next) => {
if (req.session.role == "admin") {
next();
} else {
res.redirect("/login");
}
};
router.get("/login", Login);
router.post("/login", submitLogin);
router.get("/register", Register);
router.post("/register", submitRegister);
router.get("/", auth, Dashboard);
router.post("/", auth, searchDashboard);
router.post("/:search", auth, searchDashboard);
router.get("/apps", auth, Apps);
router.get("/apps/:page", auth, Apps);
router.get("/users", auth, Users);
router.get("/images", auth, Images);
router.get("/networks", auth, Networks);
router.get("/volumes", auth, Volumes);
router.get("/syslogs", auth, Syslogs);
router.get("/account", Account);
router.get("/settings", auth, Settings);
router.get("/logout", (req, res) => {
const sessionId = req.session.id;
req.session.destroy(() => {
io.to(sessionId).disconnectSockets();
res.redirect("/login");
});
});

View file

@ -1,59 +0,0 @@
const express = require("express");
const router = express.Router();
const { Dashboard, AddSite, RemoveSite, RefreshSites, DisableSite, EnableSite } = require("../controllers/dashboard");
const { Login, processLogin, Logout, Register, processRegister } = require("../controllers/auth");
const { Apps, searchApps, Install, Uninstall } = require("../controllers/apps");
const { Images } = require("../controllers/images");
const { Volumes } = require("../controllers/volumes");
const { Networks } = require("../controllers/networks");
const { Users } = require("../controllers/users");
const { Account } = require("../controllers/account");
const { Settings } = require("../controllers/settings");
// Authentication middleware
const authenticate = (req, res, next) => {
if (req.session && req.session.role == "admin") {
console.log(`User ${req.session.user} [${req.session.role}] accessed ${req.originalUrl}`)
next();
} else {
console.log('Not admin')
res.redirect("/login");
}
};
// Dashboard
router.get("/", authenticate, Dashboard);
router.post("/addsite", authenticate, AddSite);
router.post("/removesite", authenticate, RemoveSite);
router.get("/refreshsites", authenticate, RefreshSites);
router.post("/disablesite", authenticate, DisableSite);
router.post("/enablesite", authenticate, EnableSite);
router.get("/images", authenticate, Images);
// Auth
router.get("/login", Login);
router.post("/login", processLogin);
router.get("/register", Register);
router.post("/register", processRegister);
router.get("/logout", Logout);
// Apps page
router.get("/apps", authenticate, Apps);
router.get("/apps/:page", authenticate, Apps);
router.get("/apps/:template/:page", authenticate, Apps);
router.post("/apps", authenticate, searchApps);
// Settings page
router.get("/settings", authenticate, Settings);
router.get("/account", authenticate, Account);
router.post("/install", authenticate, Install);
router.post("/uninstall", authenticate, Uninstall);
router.get("/users", authenticate, Users);
module.exports = router;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

View file

@ -1,70 +0,0 @@
#!/bin/bash
# To demo DweebUI, run this script on a fresh Debian 12.2 install. This script will open port 443/tcp for Reverse Proxy and 22/tcp for SSH.
# Manual Install:
# cd DweebUI
# chmod +x setup.sh
# sudo ./setup.sh
# Install dependencies
apt-get install -y curl unzip ufw gnupg ca-certificates lsb-release gpg
# Enable firewall
ufw allow ssh && ufw --force enable
# Opens port 443/tcp for Reverse Proxy
ufw allow https
# Install Docker
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -y
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Create docker network
docker network create -d bridge AppBridge
# Create a redis docker container with persistent storage and a password
docker run -d --name DweebCache --restart unless-stopped -v /home/docker/redis:/data -p 6379:6379 redis redis-server --requirepass "somesupersecretpassword"
# Install redis
# curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
# echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
# apt-get update -y
# apt-get install -y redis
# systemctl enable --now redis-server
# Install nodejs
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
NODE_MAJOR=20
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt-get update
sudo apt-get install nodejs -y
npm i
# Prep for caddy
mkdir -p /home/docker/caddy/sites
echo "import sites/*" > /home/docker/caddy/Caddyfile.tmp
mv /home/docker/caddy/Caddyfile.tmp /home/docker/caddy/Caddyfile
# Install pm2 and start DweebUI
npm install pm2 -g
pm2 start app.js --name "dweebui"
pm2 log
# Creates a 'docker-compose' alias, since the command changed to 'docker compose' in Debian 11.
echo '#!/bin/sh
docker compose "$@"' > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

Some files were not shown because too many files have changed in this diff Show more