Merge pull request #47 from lllllllillllllillll/dev
v0.20 - The rewrite
3
.github/FUNDING.yml
vendored
|
@ -1,2 +1 @@
|
|||
github: [lllllllillllllillll]
|
||||
patreon: DweebUI
|
||||
patreon: DweebUI
|
2
.github/dependabot.yml
vendored
|
@ -15,6 +15,6 @@ updates:
|
|||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "🤖 Dependencies"
|
||||
|
|
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
**/node_modules/
|
||||
**/database.sqlite
|
||||
**/appdata/
|
||||
.github
|
||||
test
|
||||
.dockerignore
|
||||
.gitignore
|
||||
docker-compose.yaml
|
29
CHANGELOG.md
|
@ -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.
|
||||
|
|
|
@ -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"]
|
||||
|
|
64
README.md
|
@ -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)
|
||||
|
||||
[](https://github.com/lllllllillllllillll)
|
||||
[](https://github.com/lllllllillllllillll)
|
||||
[](https://hub.docker.com/repository/docker/lllllllillllllillll/dweebui)
|
||||
[](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
|
||||
[](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
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
import ./sites/*
|
|
@ -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
|
@ -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>`;
|
||||
}
|
|
@ -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 };
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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.",
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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
|
||||
});
|
||||
|
||||
}
|
|
@ -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
|
@ -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
|
||||
});
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
@ -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
|
||||
},
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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('/');
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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
32
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.meter {
|
||||
box-sizing: content-box;
|
||||
height: 15px; /* Can be anything */
|
||||
height: 15px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: relative;
|
||||
|
|
72
public/css/tabler.min.css
vendored
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
BIN
public/fonts/Inter-Black.woff2
Normal file
BIN
public/fonts/Inter-BlackItalic.woff2
Normal file
BIN
public/fonts/Inter-Bold.woff2
Normal file
BIN
public/fonts/Inter-BoldItalic.woff2
Normal file
BIN
public/fonts/Inter-ExtraBold.woff2
Normal file
BIN
public/fonts/Inter-ExtraBoldItalic.woff2
Normal file
BIN
public/fonts/Inter-ExtraLight.woff2
Normal file
BIN
public/fonts/Inter-ExtraLightItalic.woff2
Normal file
BIN
public/fonts/Inter-Italic.woff2
Normal file
BIN
public/fonts/Inter-Light.woff2
Normal file
BIN
public/fonts/Inter-LightItalic.woff2
Normal file
BIN
public/fonts/Inter-Medium.woff2
Normal file
BIN
public/fonts/Inter-MediumItalic.woff2
Normal file
BIN
public/fonts/Inter-Regular.woff2
Normal file
BIN
public/fonts/Inter-SemiBold.woff2
Normal file
BIN
public/fonts/Inter-SemiBoldItalic.woff2
Normal file
BIN
public/fonts/Inter-Thin.woff2
Normal file
BIN
public/fonts/Inter-ThinItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Black.woff2
Normal file
BIN
public/fonts/InterDisplay-BlackItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Bold.woff2
Normal file
BIN
public/fonts/InterDisplay-BoldItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-ExtraBold.woff2
Normal file
BIN
public/fonts/InterDisplay-ExtraBoldItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-ExtraLight.woff2
Normal file
BIN
public/fonts/InterDisplay-ExtraLightItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Italic.woff2
Normal file
BIN
public/fonts/InterDisplay-Light.woff2
Normal file
BIN
public/fonts/InterDisplay-LightItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Medium.woff2
Normal file
BIN
public/fonts/InterDisplay-MediumItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Regular.woff2
Normal file
BIN
public/fonts/InterDisplay-SemiBold.woff2
Normal file
BIN
public/fonts/InterDisplay-SemiBoldItalic.woff2
Normal file
BIN
public/fonts/InterDisplay-Thin.woff2
Normal file
BIN
public/fonts/InterDisplay-ThinItalic.woff2
Normal file
BIN
public/fonts/InterVariable-Italic.woff2
Normal file
BIN
public/fonts/InterVariable.woff2
Normal file
59
public/fonts/inter.css
Normal 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"); }
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
BIN
public/img/avatars/duffman.png
Normal file
After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
BIN
public/img/avatars/moleman.png
Normal file
After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
@ -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) {
|
||||
|
|
|
@ -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
1
public/libs/list.js/dist/list.js.map
vendored
Normal file
2
public/libs/list.js/dist/list.min.js
vendored
Normal file
1
public/libs/list.js/dist/list.min.js.map
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"list.min.js","sources":["webpack://List/list.min.js"],"mappings":"AAAA","sourceRoot":""}
|
1
public/static/logo-sm-black.svg
Normal file
After Width: | Height: | Size: 15 KiB |
1
public/static/logo-sm-white.svg
Normal file
After Width: | Height: | Size: 15 KiB |
|
@ -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 |
Before Width: | Height: | Size: 5.5 KiB |
|
@ -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 |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 413 B |
61
router/index.js
Normal 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);
|
|
@ -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;
|