Merge pull request #47 from lllllllillllllillll/dev

v0.20 - The rewrite
This commit is contained in:
lllllllillllllillll 2024-01-20 15:36:14 -08:00 committed by GitHub
commit c3f10fbb7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
128 changed files with 8618 additions and 3936 deletions

3
.github/FUNDING.yml vendored
View file

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

View file

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

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
**/node_modules/
**/database.sqlite
**/appdata/
.github
test
.dockerignore
.gitignore
docker-compose.yaml

View file

@ -1,3 +1,32 @@
## v0.20 (Jan 20th 2024) - 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 with more users.
* Pages for images, volumes, and networks. :new:
* Localized fonts.
* CORS.
* Testing with Mocha and Supertest.
* Created Portal page. :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.
* Added Helmet.
* Fixed missing session data.
* Reduced sqlite queries.
## v0.08 (Dec 15th 2023)
* Updates to compose file and instructions from [steveiliop56](https://github.com/steveiliop56)
* Added SECRET field to compose file as a basic security measure.

View file

@ -1,20 +1,19 @@
# syntax=docker/dockerfile:1
FROM node:21-alpine
ENV NODE_ENV=production
WORKDIR /app
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci --omit=dev
USER root
COPY . .
EXPOSE 8000
CMD node app.js
CMD ["node", "server.js"]

View file

@ -1,39 +1,46 @@
# 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 controlling and monitoring your containers.
Pre-Pre-Pre-Pre-Pre Alpha v0.08 ( :fire: Experimental. Don't install on any servers you care about :fire: )
Alpha v0.20 ( :fire: Experimental :fire: )
[:warning: DweebUI is a management interface and should not be directly exposed to the internet :warning:](https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet)
[![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)
[![GitHub License](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/lllllllillllllillll)
* This is a personal project I started to get more familiar with Javascript and Node.js.
* Some UI elements are placeholders and every version may have breaking changes.
* Please post issues and discussions so I know what bugs and features to focus on.
* This is a personal project that I decided to share. I'm sure it has plenty of bugs and mistakes.
* I haven't used Github very much and I'm still new to Javascript.
* I probably should have waited a lot longer to share this :|
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard1.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard1.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard2.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard2.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/images.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/images.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/register.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/register.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/login.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/login.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/syslogs.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/syslogs.png" width="25%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/volumes.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/volumes.png" width="25%"/></a>
<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>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png" width="50%"/></a>
## Features
* [x] Dashboard provides server metrics, container metrics, and container controls, on a single page.
* [x] View container logs.
* [ ] Update containers (planned).
* [ ] Manage your Docker networks, images, and volumes (planned).
* [ ] Manage your Docker networks, images, and volumes (in development).
* [x] Light/Dark Mode.
* [x] Easy to install app templates.
* [x] Proxy manager for Caddy (Optional).
* [x] Multi-User built-in.
* [ ] User pages (planned).
* [ ] Permissions system (in development).
* [x] Support for Windows, Linux, and MacOS.
* [ ] Docker compose support (planned).
* [ ] Docker compose import (in development).
* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
* [x] Automatically persists data in docker volumes if bind mount isn't used.
* [ ] Preset variables (planned).
* [ ] Offline/Local Icons (planned).
## Setup
@ -42,37 +49,27 @@ Docker Compose:
```
version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.08
# build:
# context: .
image: lllllllillllllillll/dweebui:v0.20
environment:
NODE_ENV: production
PORT: 8000
SECRET: MrWiskers
#Proxy_Manager: enabled
restart: unless-stopped
ports:
- 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
```
@ -83,18 +80,15 @@ 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
* Dockerode and dockerode-compose by Apocas: https://github.com/apocas/dockerode
* UI was built using HTML and CSS elements from https://tabler.io/
* Apps template based on Portainer template provided by Lissy93: https://github.com/Lissy93/portainer-templates
* Icons from Walkxcode with some renames and additions: https://github.com/walkxcode/dashboard-icons
* Icons from Walkxcode with some renames and additions: https://github.com/walkxcode/dashboard-icons
## Supporters
* MM (Patreon)

143
app.js
View file

@ -1,143 +0,0 @@
// Express
const express = require("express");
const app = express();
const session = require("express-session");
const PORT = process.env.PORT || 8000;
// Router
const routes = require("./routes");
// Functions and variables
const { serverStats, containerList, containerStats, containerAction, containerLogs, hiddenContainers } = require('./functions/system');
let sentList, clicked;
app.locals.site_list = '';
const Containers = require('./database/ContainerSettings');
// Configure Session
const sessionMiddleware = session({
secret: "keyboard cat",
resave: false,
saveUninitialized: false,
cookie:{
secure:false, // Only set to true if you are using HTTPS.
httpOnly:false, // Only set to true if you are using HTTPS.
maxAge:3600000 * 8 // Session max age in milliseconds. 3600000 = 1 hour.
}
})
// Middleware
app.set('view engine', 'ejs');
app.use([
express.static("public"),
express.json(),
express.urlencoded({ extended: true }),
sessionMiddleware,
routes
]);
// Start Express server
const server = app.listen(PORT, async () => {
console.log(`App listening on port ${PORT}`);
});
// Start Socket.io
const io = require('socket.io')(server);
io.engine.use(sessionMiddleware);
io.on('connection', (socket) => {
// 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); }
// 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);
}
}, 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
}
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
});
hiddenContainers();
console.log(`[Created] Container ${data.container} hidden`)
} else {
containerExists.update({ visibility: false });
console.log(`[Updated] Container ${data.container} hidden`)
hiddenContainers();
}
});
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);
});
});

View file

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

View file

@ -1,6 +1,7 @@
function appCard(data) {
export const appCard = (data) => {
// make data.title lowercase
// dont look at anything in here.
let app_name = data.name || data.title.toLowerCase();
let shortened_name = "";
let shortened_desc = data.description.slice(0, 60) + "...";
@ -15,11 +16,10 @@ function appCard(data) {
let repository = data.repository || "";
let source = data.image || "";
// if data.network is set to host, bridge, or docker set the radio button to checked
let net_host, net_bridge, net_docker = '';
let net_name = 'AppBridge';
// if data.network is set to host, bridge, or docker set the radio button to checked
if (data.network == 'host') {
net_host = 'checked';
} else if (data.network) {
@ -219,7 +219,7 @@ function appCard(data) {
<div class="col-md-6 col-lg-3">
<div class="card">
<div class="card-body p-4 text-center">
<span class="avatar avatar-xlplus mb-3 rounded"><img src='${data.logo}' width="144px" height="144px" loading="lazy"></img></span>
<span class="avatar avatar-xlplus mb-3 rounded"><img src='${data.logo}' width="144px" height="144px" loading="lazy"/></span>
<h3 class="m-0 mb-1"><a href="#">${shortened_name}</a></h3>
<div class="text-secondary">${shortened_desc}</div>
<div class="mt-3">
@ -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>
@ -989,6 +989,4 @@ function appCard(data) {
</div>`;
}
module.exports = { appCard };
}

285
components/containerCard.js Normal file
View file

@ -0,0 +1,285 @@
// 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="#${name}_uninstall_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 text-secondary" onclick="clicked(this)" name="${name}" id="hide" value="hide">Hide</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="resetView" value="resetView">Reset View</button>
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="permissions" value="permissions" data-bs-toggle="modal" data-bs-target="#${name}_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>
<div class="modal modal-blur fade" id="${name}_uninstall_modal" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-status bg-danger"></div>
<div class="modal-body text-center py-3">
<svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" 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 9v2m0 4v.01"></path><path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path></svg>
<h3>Remove ${name}?</h3>
<form action="/uninstall" id="${name}_uninstall" method="POST">
<input type="text" class="form-control" name="service_name" value="${name}" hidden/>
<div class="mb-3"> </div>
<div class="mb-2">
<div class="divide-y">
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">Remove Volumes</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_volumes" disabled="">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Remove Image
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_image" disabled="">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Remove Backups
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_backups" disabled="">
</label>
</div>
</div>
</div>
</div>
<div class="mt-1"> </div>
<div class="text-muted">Enter "Yes" below to remove the container.</div>
<input type="text" class="form-control mb-2" name="confirm" autocomplete="off">
</form>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col">
<a href="#" class="btn w-100" data-bs-dismiss="modal">
Cancel
</a>
</div>
<div class="col">
<input type="submit" form="${name}_uninstall" class="btn btn-danger w-100" value="Uninstall"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal modal-blur fade" id="${name}_permissions" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-status bg-cyan"></div>
<div class="modal-body text-center py-3">
<h3>${name} permissions</h3>
<form action="#" id="${name}_permissions" method="POST">
<input type="text" class="form-control" name="service_name" value="${name}" hidden/>
<div class="mb-2">
<div class="divide-y">
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">Install</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_volumes">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Uninstall
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_image">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Edit
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_backups">
</label>
</div>
</div>
<div class="row">
<div class="col-9">
<label class="row text-start">
<span class="col">
Upgrade
</span>
</label>
</div>
<div class="col-3">
<label class="form-check form-check-single form-switch text-end">
<input class="form-check-input" type="checkbox" name="remove_backups">
</label>
</div>
</div>
</div>
</div>
<div class="mt-1"> </div>
</form>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col">
<a href="#" class="btn w-100" data-bs-dismiss="modal">
Cancel
</a>
</div>
<div class="col">
<input type="submit" form="${name}_permissions" class="btn btn-primary w-100" value="Update"/>
</div>
</div>
</div>
</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) => {
let user = await User.findOne({ where: { UUID: req.session.UUID }});
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,
});
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,
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,190 +1,98 @@
const User = require('../database/UserModel');
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);
let last_page = Math.ceil(templates.length/28);
if (req.session.role == "admin") {
// Get the user.
let user = await User.findOne({ where: { UUID: req.session.UUID }});
let page = Number(req.params.page) || 1;
let list_start = (page - 1) * 28;
let list_end = (page * 28);
let last_page = Math.ceil(templates.length / 28);
let prev = '/apps/' + (page - 1);
let next = '/apps/' + (page + 1);
if (page == 1) {
prev = '/apps/' + (page);
}
if (page == last_page) {
next = '/apps/' + (page);
}
let apps_list = '';
for (let i = list_start; i < list_end && i < templates.length; i++) {
let app_card = appCard(templates[i]);
apps_list += app_card;
}
// Render the home page
res.render("pages/apps", {
name: user.first_name + ' ' + user.last_name,
role: user.role,
avatar: user.avatar,
isLoggedIn: true,
list_start: list_start + 1,
list_end: list_end,
app_count: templates.length,
prev: prev,
next: next,
apps_list: apps_list
});
} else {
// Redirect to the login page
res.redirect("/login");
let prev = '/apps/' + (page-1);
let next = '/apps/' + (page+1);
if (page == 1) {
prev = '/apps/' + (page);
}
if (page == last_page) {
next = '/apps/' + (page);
}
let apps_list = '';
for (let i = list_start; i < list_end && i < templates.length; i++) {
let app_card = appCard(templates[i]);
apps_list += app_card;
}
res.render("apps", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
list_start: list_start + 1,
list_end: list_end,
app_count: templates.length,
prev: prev,
next: next,
apps_list: apps_list
});
}
exports.searchApps = async function(req, res) {
if (req.session.role == "admin") {
export const appSearch = async (req, res) => {
// Get the user.
let user = await User.findOne({ where: { UUID: req.session.UUID }});
let search = req.body.search.split(' ');
let apps_list = '';
let results = [];
let page = Number(req.query.page) || 1;
let list_start = (page - 1) * 28;
let list_end = (page * 28);
let last_page = Math.ceil(templates.length / 28);
let page = Number(req.query.page) || 1;
let list_start = (page - 1) * 28;
let list_end = (page * 28);
let last_page = Math.ceil(templates.length / 28);
let prev = '/apps?page=' + (page - 1);
let next = '/apps?page=' + (page + 1);
if (page == 1) {
prev = '/apps?page=' + (page);
}
if (page == last_page) {
next = '/apps?page=' + (page);
}
let prev = '/apps?page=' + (page - 1);
let next = '/apps?page=' + (page + 1);
if (page == 1) {
prev = '/apps?page=' + (page);
}
if (page == last_page) {
next = '/apps?page=' + (page);
}
let apps_list = '';
let search_results = [];
let search = req.body.search;
// split value of search into an array of words
search = search.split(' ');
try {console.log(search[0]);} catch (error) {}
try {console.log(search[1]);} catch (error) {}
try {console.log(search[2]);} catch (error) {}
function searchTemplates(word) {
for (let i = 0; i < templates.length; i++) {
if ((templates[i].description.includes(word)) || (templates[i].name.includes(word)) || (templates[i].title.includes(word))) {
search_results.push(templates[i]);
}
function searchTemplates(word) {
for (let i = 0; i < templates.length; i++) {
if ((templates[i].description.includes(word)) || (templates[i].name.includes(word)) || (templates[i].title.includes(word))) {
results.push(templates[i]);
}
// console.log(search_results);
}
searchTemplates(search);
for (let i = 0; i < search_results.length; i++) {
let app_card = appCard(search_results[i]);
apps_list += app_card;
}
// Render the home page
res.render("pages/apps", {
name: user.first_name + ' ' + user.last_name,
role: user.role,
avatar: user.avatar,
isLoggedIn: true,
list_start: list_start + 1,
list_end: list_end,
app_count: templates.length,
prev: prev,
next: next,
apps_list: apps_list
});
} else {
// Redirect to the login page
res.redirect("/login");
}
}
searchTemplates(search);
exports.Install = async function (req, res) {
for (let i = 0; i < results.length; i++) {
let app_card = appCard(results[i]);
apps_list += app_card;
}
if (req.session.role == "admin") {
res.render("apps", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
list_start: list_start + 1,
list_end: list_end,
app_count: templates.length,
prev: prev,
next: next,
apps_list: apps_list
});
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,150 +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;
// 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}">`
});
// set the session.
req.session.user = user.username;
req.session.UUID = user.UUID;
req.session.role = user.role;
// 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,249 +1,23 @@
const User = require('../database/UserModel');
const Containers = require('../database/ContainerSettings');
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) => {
res.render("dashboard", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
});
exports.Dashboard = async function (req, res) {
if (req.session.role == "admin") {
// get user data with matching UUID from sqlite database
let user = await User.findOne({ where: { UUID: req.session.UUID } });
let caddy = 'd-none';
if (process.env.Proxy_Manager == 'enabled') {
caddy = '';
}
// Render the home page
res.render("pages/dashboard", {
name: user.first_name + ' ' + user.last_name,
role: user.role,
avatar: user.avatar,
isLoggedIn: true,
site_list: req.app.locals.site_list,
caddy: caddy
});
} else {
// Redirect to the login page
res.redirect("/login");
}
}
export const searchDashboard = (req, res) => {
console.log(req.params);
exports.AddSite = async function (req, res) {
res.render("dashboard", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
});
let { domain, type, host, port } = req.body;
if ((req.session.role == "admin") && ( 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('not admin or missing info')
res.redirect("/");
}
}
exports.RemoveSite = async function (req, res) {
if (req.session.role == "admin") {
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`);
});
}
let reload = {
container: 'DweebProxy',
command: `caddy reload --config /etc/caddy/Caddyfile`
}
await containerExec(reload);
console.log('Removed Site(s)')
res.redirect("/refreshsites");
} else {
res.redirect("/");
}
}
exports.RefreshSites = async function (req, res) {
let domain, type, host, port;
let id = 1;
if (req.session.role == "admin") {
// 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("/");
} else {
// Redirect to the login page
res.redirect("/");
}
}
exports.DisableSite = async function (req, res) {
if (req.session.role == "admin") {
console.log(req.body)
console.log('Disable Site')
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
}
exports.EnableSite = async function (req, res) {
if (req.session.role == "admin") {
console.log(req.body)
console.log('Enable Site')
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
}

56
controllers/images.js Normal file
View file

@ -0,0 +1,56 @@
import { docker } from '../server.js';
export const Images = async function(req, res) {
let images = await docker.listImages({ all: true });
let image_list = `
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
<th><button class="table-sort" data-sort="sort-name">Name</button></th>
<th><button class="table-sort" data-sort="sort-city">ID</button></th>
<th><button class="table-sort" data-sort="sort-type">Tag</button></th>
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
<th><button class="table-sort" data-sort="sort-quantity">Size</button></th>
<th><button class="table-sort" data-sort="sort-progress">Action</button></th>
</tr>
</thead>
<tbody class="table-tbody">`
for (let i = 0; i < images.length; i++) {
let date = new Date(images[i].Created * 1000);
let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
let size = images[i].Size / 1000 / 1000; // to match docker desktop
size = size.toFixed(2);
let details = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${images[i].RepoTags}</td>
<td class="sort-city">${images[i].Id}</td>
<td class="sort-type">Latest</td>
<td class="sort-score text-green">In use</td>
<td class="sort-date" data-date="1628122643">${created}</td>
<td class="sort-quantity">${size} MB</td>
<td class="text-end"><a class="btn" href="#">Details</a></td>
</tr>`
image_list += details;
}
image_list += `</tbody>`
res.render("images", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
image_list: image_list,
image_count: images.length
});
}

82
controllers/login.js Normal file
View file

@ -0,0 +1,82 @@
import { User, 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
});
if (req.session.role == "admin") {
res.redirect("/");
}
else {
res.redirect("/portal");
}
}else{
const syslog = await Syslog.create({
user: null,
email: email,
event: "Bad Login",
message: "Invalid password",
ip: req.socket.remoteAddress
});
res.render("login",{
"error":"Invalid password",
});
}
}else{
res.render("login",{
"error":"User with that email does not exist.",
});
}
}else{
res.status(400);
res.render("login",{
"error":"Please fill in all the fields.",
});
}
}
export const Logout = function(req,res){
req.session.destroy(() => {
res.redirect("/login");
});
}

52
controllers/networks.js Normal file
View file

@ -0,0 +1,52 @@
import { docker } from '../server.js';
export const Networks = async function(req, res) {
let networks = await docker.listNetworks({ all: true });
let network_list = `
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
<th><button class="table-sort" data-sort="sort-name">Name</button></th>
<th><button class="table-sort" data-sort="sort-city">ID</button></th>
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
<th><button class="table-sort" data-sort="sort-progress">Action</button></th>
</tr>
</thead>
<tbody class="table-tbody">`
for (let i = 0; i < networks.length; i++) {
// let date = new Date(images[i].Created * 1000);
// let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
let details = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${networks[i].Name}</td>
<td class="sort-city">${networks[i].Id}</td>
<td class="sort-score text-green">In use</td>
<td class="sort-date" data-date="1628122643">${networks[i].Created}</td>
<td class="text-end"><a class="btn" href="#">Details</a></td>
</tr>`
network_list += details;
}
network_list += `</tbody>`
res.render("networks", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
network_list: network_list,
network_count: networks.length
});
}

12
controllers/portal.js Normal file
View file

@ -0,0 +1,12 @@
export const Portal = (req, res) => {
res.render("portal", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
});
}

100
controllers/register.js Normal file
View file

@ -0,0 +1,100 @@
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, warning, 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 && warning) && (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 {
let currentDate = new Date();
let newLogin = currentDate.toLocaleString();
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}">`,
lastLogin: newLogin,
});
// 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 acknowledge security warning.",
});
}
}

View file

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

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,54 +1,60 @@
const User = require('../database/UserModel');
import { User } from '../database/models.js';
exports.Users = async function(req, res) {
if (req.session.role == "admin") {
export const Users = async (req, res) => {
let user_list = `
<tr>
<th><input class="form-check-input" type="checkbox"></th>
<th>ID</th>
<th>Avatar</th>
<th>Name</th>
<th>Username</th>
<th>Email</th>
<th>UUID</th>
<th>Role</th>
<th>Last Login</th>
<th>Status</th>
<th>Actions</th>
</tr>`
// Get the user.
let user = await User.findOne({ where: { UUID: req.session.UUID }});
let user_list = `
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>
<th><input class="form-check-input" type="checkbox"></th>
<th>ID</th>
<th>Avatar</th>
<th>Name</th>
<th>Username</th>
<th>Email</th>
<th>UUID</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
<td><input class="form-check-input" type="checkbox"></td>
<td>${account.id}</td>
<td><span class="avatar me-2">${account.avatar}</span></td>
<td>${account.name}</td>
<td>${account.username}</td>
<td>${account.email}</td>
<td>${account.UUID}</td>
<td>${account.role}</td>
<td>${account.lastLogin}</td>
<td>${active}</td>
<td><a href="#" class="btn">Edit</a></td>
</tr>`
let users = await User.findAll();
users.forEach((account) => {
full_name = account.first_name + ' ' + account.last_name;
user_info = `
<tr>
<td><input class="form-check-input" type="checkbox"></td>
<td>${user.id}</td>
<td><span class="avatar me-2">${account.avatar}</span></td>
<td>${full_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><a href="#" class="btn">Edit</a></td>
</tr>`
user_list += info;
});
user_list += user_info;
});
// Render the home page
res.render("pages/users", {
name: user.first_name + ' ' + user.last_name,
role: user.role,
avatar: user.avatar,
isLoggedIn: true,
user_list: user_list
});
} else {
// Redirect to the login page
res.redirect("/login");
}
res.render("users", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
user_list: user_list
});
}

70
controllers/volumes.js Normal file
View file

@ -0,0 +1,70 @@
import { docker } from '../server.js';
export const Volumes = async function(req, res) {
let list = await docker.listVolumes({ all: true });
let volumes = list.Volumes;
let volume_list = `
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
<th><button class="table-sort" data-sort="sort-name">Name</button></th>
<th><button class="table-sort" data-sort="sort-city">Mount point</button></th>
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
<th><button class="table-sort" data-sort="sort-quantity">Size</button></th>
<th><button class="table-sort" data-sort="sort-progress">Action</button></th>
</tr>
</thead>
<tbody class="table-tbody">`
for (let i = 0; i < volumes.length; i++) {
let volume = volumes[i];
let name = volume.Name;
let mount = volume.Mountpoint;
if (name.length > 40) {
name = name.slice(0, 37) + '...';
}
if (mount.length > 70) {
mount = mount.slice(0, 67) + '...';
}
// docker.df(volume.Mountpoint).then((data) => {
// for (let key in data) {
// console.log(data[key]);
// }
// });
let details = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${name}</td>
<td class="sort-city">${mount}</td>
<td class="sort-score text-green">In use</td>
<td class="sort-date" data-date="1628122643">${volume.CreatedAt}</td>
<td class="sort-quantity">MB</td>
<td class="text-end"><a class="btn" href="#">Details</a></td>
</tr>`
volume_list += details;
}
volume_list += `</tbody>`
res.render("volumes", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
volume_list: volume_list,
volume_count: volumes.length
});
}

View file

@ -1,47 +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
},
permissions: {
type: DataTypes.STRING
// allowNull defaults to true
}
});
async function syncModel() {
await sequelize.sync();
console.log('Containers model synced');
}
syncModel();
module.exports = Containers;

View file

@ -1,42 +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
}
});
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;

189
database/models.js Normal file
View file

@ -0,0 +1,189 @@
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
},
});
export const Notification = sequelize.define('Notification', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
title: {
type: DataTypes.STRING
},
message: {
type: DataTypes.STRING
},
icon: {
type: DataTypes.STRING,
},
color: {
type: DataTypes.STRING,
},
createdAt : {
type: DataTypes.STRING
},
createdBy : {
type: DataTypes.STRING
},
});

View file

@ -2,30 +2,23 @@ version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.08
# build:
# context: .
image: lllllllillllllillll/dweebui:v0.20
environment:
NODE_ENV: production
PORT: 8000
SECRET: MrWiskers
#Proxy_Manager: enabled
restart: unless-stopped
ports:
- 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) {
});
}
}
}

208
functions/install.js Normal file
View file

@ -0,0 +1,208 @@
import { writeFileSync, mkdirSync, readFileSync } from "fs";
import yaml from 'js-yaml';
import { execSync } from "child_process";
import { docker } from "../server.js";
import DockerodeCompose from "dockerode-compose";
import { Syslog } from "../database/models.js";
import { containerCard } from "../components/containerCard.js";
// This entire page hurts to look at.
export const Install = async (req, res) => {
console.log(req.app.locals.installCard);
let data = req.body;
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 ports = [port0, port1, port2, port3, port4, port5]
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
for (let i = 0; i < ports.length; i++) {
if ((ports[i] == 'on') && (net_mode != 'host')) {
compose_file += `\n ports:`
break;
}
}
for (let i = 0; i < ports.length; i++) {
if ((ports[i] == 'on') && (net_mode != 'host')) {
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
}
}
// Volumes
let volumes = [volume0, volume1, volume2, volume3, volume4, volume5]
for (let i = 0; i < volumes.length; i++) {
if (volumes[i] == 'on') {
compose_file += `\n volumes:`
break;
}
}
for (let i = 0; i < volumes.length; 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
let env_vars = [env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11]
for (let i = 0; i < env_vars.length; i++) {
if (env_vars[i] == 'on') {
compose_file += `\n environment:`
break;
}
}
for (let i = 0; i < env_vars.length; i++) {
if (env_vars[i] == 'on') {
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
}
}
// Labels
let labels = [label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11]
for (let i = 0; i < labels.length; i++) {
if (labels[i] == 'on') {
compose_file += `\n labels:`
break;
}
}
for (let i = 0; i < 12; i++) {
if (data[`label${i}`] == 'on') {
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
}
}
// Privileged mode
if (data.privileged == 'on') {
compose_file += `\n privileged: true`
}
// Hardware acceleration
for (let i = 0; i < env_vars.length; i++) {
if ((env_vars[i] == 'on') && (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]`
break;
}
}
// 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') }
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
(async () => {
try {
await compose.pull();
await compose.up().then(() => {
const syslog = Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installed successfully`,
ip: req.socket.remoteAddress
});
});
} catch (err) {
console.error(err);
const syslog = await Syslog.create({
user: req.session.user,
email: null,
event: "App Installation",
message: `${name} installation failed: ${err}`,
ip: req.socket.remoteAddress
});
}
})();
}
res.redirect('/');
}

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,315 +0,0 @@
const { currentLoad, mem, networkStats, fsSize, dockerContainerStats, networkInterfaces } = 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/ContainerSettings');
// 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);
});
});
});
};

41
functions/uninstall.js Normal file
View file

@ -0,0 +1,41 @@
import { docker } from "../server.js";
import { Syslog } from "../database/models.js";
export const Uninstall = async (req, res) => {
let { confirm, service_name } = req.body;
if (confirm == 'Yes') {
var containerName = docker.getContainer(`${service_name}`);
try {
await containerName.stop();
} catch {
console.log(`Error stopping ${service_name} container`);
}
try {
await containerName.remove();
const syslog = await Syslog.create({
user: req.session.user,
email: null,
event: "App Removal",
message: `${service_name} uninstalled successfully`,
ip: req.socket.remoteAddress
});
} catch {
const syslog = await Syslog.create({
user: req.session.user,
email: null,
event: "App Removal",
message: `${service_name} uninstallation failed`,
ip: req.socket.remoteAddress
});
}
}
res.redirect('/');
}

2682
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,37 @@
{
"name": "dweebui",
"version": "1.0.0",
"main": "app.js",
"description": "A web UI for Docker",
"main": "server.js",
"type": "module",
"scripts": {
"test": "mocha --require @babel/register"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.0",
"child_process": "^1.0.2",
"dockerode": "^4.0.0",
"@babel/register": "^7.23.7",
"@socket.io/admin-ui": "^0.5.1",
"bcrypt": "^5.1.1",
"chai": "^5.0.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dockerode": "^4.0.2",
"dockerode-compose": "^1.4.0",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"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",
"sqlite3": "^5.1.6",
"systeminformation": "^5.21.20"
},
"description": ""
"sinon": "^17.0.1",
"socket.io": "^4.7.4",
"sqlite3": "^5.1.7",
"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,10 @@
// SOCKET IO
const socket = io({
auth: {
token: "abc"
}
socket.on('connect', () => {
console.log('connected');
//clear localStorage (because of code in old versions)
localStorage.clear();
});
// 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 +14,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,165 +39,103 @@ 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}`);
Array.from(elements).forEach(function(element) {
if (window.ApexCharts) {
new ApexCharts(element, {
chart: {
type: "line",
fontFamily: 'inherit',
height: 40.0,
sparkline: {
enabled: true
},
animations: {
enabled: false
}
},
fill: {
opacity: 1
},
stroke: {
width: [2, 1],
dashArray: [0, 3],
lineCap: "round",
curve: "smooth"
},
series: [{
name: "CPU",
data: cpu_array
}, {
name: "RAM",
data: ram_array
}],
tooltip: {
theme: 'dark'
},
grid: {
strokeDashArray: 4
},
xaxis: {
labels: {
padding: 0
},
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();
}
});
}
// container button actions
function buttonAction(button) {
socket.emit('clicked', {container: button.name, state: button.id, action: 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>`;
// Container cards
socket.on('containers', (data) => {
let deleteMeElements = document.querySelectorAll('.deleteme');
deleteMeElements.forEach((element) => {
element.parentNode.removeChild(element);
});
dockerCards.insertAdjacentHTML("afterend", data);
});
function drawCharts(name, cpuArray, ramArray) {
let element = document.querySelector(`${name}`);
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);
let chart = new ApexCharts(element, {
chart: {
type: "line",
height: 40.0,
sparkline: {
enabled: true
},
animations: {
enabled: false
}
},
fill: {
opacity: 1
},
stroke: {
width: [2, 1],
dashArray: [0, 3],
lineCap: "round",
curve: "smooth"
},
series: [{
name: "CPU",
data: cpuArray
}, {
name: "RAM",
data: ramArray
}],
tooltip: {
enabled: false
},
grid: {
strokeDashArray: 4
},
xaxis: {
labels: {
padding: 0
},
tooltip: {
enabled: false
}
},
yaxis: {
labels: {
padding: 4
}
},
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
legend: {
show: false
}
}
});
})
chart.render();
}
// Buttons functions
function clicked(button) {
socket.emit('clicked', {name: button.name, id: button.id, value: button.value});
}
socket.on('containerStats', (data) => {
let {name, cpu, ram} = data;
console.log(`drawing chart for ${name}`)
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);
let containerStats = data;
cpu_array = cpu_array.slice(-30);
ram_array = ram_array.slice(-30);
for (const [name, statsArray] of Object.entries(containerStats)) {
localStorage.setItem(`${name}_cpu`, JSON.stringify(cpu_array));
localStorage.setItem(`${name}_ram`, JSON.stringify(ram_array));
let cpuArray = statsArray.cpuArray;
let ramArray = statsArray.ramArray;
// 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);
} else {
console.log(`Chart element with id ${name}_chart not found in the DOM`);
let chart = document.getElementById(`${name}_chart`);
if (chart) {
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('logs', (data) => {
logViewer.innerHTML = `<pre>${data}</pre>`;
});

2020
public/libs/list.js/dist/list.js vendored Normal file

File diff suppressed because it is too large Load diff

1
public/libs/list.js/dist/list.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
public/libs/list.js/dist/list.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"list.min.js","sources":["webpack://List/list.min.js"],"mappings":"AAAA","sourceRoot":""}

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: 413 B

61
router/index.js Normal file
View file

@ -0,0 +1,61 @@
import express from "express";
export const router = express.Router();
// Controllers
import { Login, submitLogin, Logout } from "../controllers/login.js";
import { Register, submitRegister } from "../controllers/register.js";
import { Dashboard, searchDashboard } from "../controllers/dashboard.js";
import { Apps, appSearch } 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";
import { Portal } from "../controllers/portal.js"
/// Functions
import { Install } from "../functions/install.js"
import { Uninstall } from "../functions/uninstall.js"
// Auth middleware
const auth = (req, res, next) => {
if (req.session.role == "admin") {
next();
} else {
res.redirect("/login");
}
};
// Routes
router.get("/login", Login);
router.post("/login", submitLogin);
router.get("/logout", Logout);
router.get("/register", Register);
router.post("/register", submitRegister);
router.get("/", auth, Dashboard);
router.post("/", auth, searchDashboard);
router.get("/images", auth, Images);
router.get("/volumes", auth, Volumes);
router.get("/networks", auth, Networks);
router.get("/portal", Portal)
router.get("/apps", auth, Apps);
router.get("/apps/:page", auth, Apps);
router.post("/apps", auth, appSearch);
router.get("/users", auth, Users);
router.get("/syslogs", auth, Syslogs);
router.get("/account", Account);
router.get("/settings", auth, Settings);
// Functions
router.post("/install", auth, Install);
router.post("/uninstall", auth, Uninstall);

View file

@ -1,48 +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 { Users } = require("../controllers/users");
const { Account } = require("../controllers/account");
const { Settings } = require("../controllers/settings");
// Dashboard
router.get("/", Dashboard);
router.post("/addsite", AddSite)
router.post("/removesite", RemoveSite)
router.get("/refreshsites", RefreshSites)
router.post("/disablesite", DisableSite)
router.post("/enablesite", EnableSite)
// 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", Apps);
router.get("/apps/:page", Apps);
router.get("/apps/:template/:page", Apps);
router.post("/apps", searchApps);
// Settings page
router.get("/settings", Settings);
router.get("/account", Account);
router.post("/install", Install)
router.post("/uninstall", Uninstall)
router.get("/users", Users);
module.exports = router;

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