diff --git a/CHANGELOG.md b/CHANGELOG.md index d609cac..728a5e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## v0.70 (dev) +* + ## v0.60 (June 9th 2024) - Permissions system and import templates * Converted JS template literals into HTML. * Converted modals into HTML/HTMX. diff --git a/controllers/dashboard.js b/controllers/dashboard.js index f016b83..c1abe38 100644 --- a/controllers/dashboard.js +++ b/controllers/dashboard.js @@ -1,9 +1,8 @@ import { Readable } from 'stream'; import { Permission, User } from '../database/models.js'; import { docker } from '../server.js'; -import { dockerContainerStats } from 'systeminformation'; import { readFileSync } from 'fs'; -import { currentLoad, mem, networkStats, fsSize } from 'systeminformation'; +import { currentLoad, mem, networkStats, fsSize, dockerContainerStats } from 'systeminformation'; import { Op } from 'sequelize'; let hidden = ''; diff --git a/public/css/tabler.min.css b/public/css/tabler.min.css index 33fa9d3..714fbcf 100644 --- a/public/css/tabler.min.css +++ b/public/css/tabler.min.css @@ -8,6 +8,13 @@ */ @charset "UTF-8"; + +.form-label:focus, +.form-control:focus { + outline: none; + box-shadow: none; +} + :root, [data-bs-theme=light] { --tblr-blue: #206bc4; @@ -3187,13 +3194,7 @@ progress { cursor: pointer } -.form-control:focus { - color: inherit; - background-color: var(--tblr-bg-forms); - border-color: #90b5e2; - outline: 0; - box-shadow: 0 0 transparent, 0 0 0 .25rem rgba(32, 107, 196, .25) -} + .form-control::-webkit-date-and-time-value { min-width: 85px; @@ -3417,11 +3418,6 @@ textarea.form-control-lg { } } -.form-select:focus { - border-color: #90b5e2; - outline: 0; - box-shadow: 0 0 transparent, 0 0 0 .25rem rgba(32, 107, 196, .25) -} .form-select[multiple], .form-select[size]:not([size="1"]) { @@ -3514,11 +3510,6 @@ textarea.form-control-lg { filter: brightness(90%) } -.form-check-input:focus { - border-color: #90b5e2; - outline: 0; - box-shadow: 0 0 0 .25rem rgba(32, 107, 196, .25) -} .form-check-input:checked { background-color: var(--tblr-primary); @@ -3571,7 +3562,7 @@ textarea.form-control-lg { } } -.form-switch .form-check-input:focus { +.form-switch .form-check-input { --tblr-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2390b5e2'/%3e%3c/svg%3e") } @@ -24270,10 +24261,7 @@ textarea[cols] { border-color: #90b5e2 !important } -.input-group-flat .form-control:focus { - border-color: var(--tblr-border-color); - box-shadow: none -} + .input-group-flat .form-control:not(:last-child) { border-right: 0 diff --git a/server.js b/server.js index 6ffc603..635ebe3 100644 --- a/server.js +++ b/server.js @@ -2,10 +2,10 @@ import express from 'express'; import session from 'express-session'; import memorystore from 'memorystore'; import ejs from 'ejs'; -import Docker from 'dockerode'; import { router } from './router/index.js'; import { sequelize } from './database/models.js'; -export const docker = new Docker(); +import Docker from 'dockerode'; +export var docker = new Docker(); // Session middleware const MemoryStore = memorystore(session); diff --git a/utils/install.js b/utils/install.js index 9a8c5f9..a276ef9 100644 --- a/utils/install.js +++ b/utils/install.js @@ -1,250 +1,224 @@ import { writeFileSync, mkdirSync, readFileSync, readdirSync, writeFile } 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 { addAlert } from "../controllers/dashboard.js"; +import { docker } from "../server.js"; +import DockerodeCompose from "dockerode-compose"; +import yaml from 'js-yaml'; + + -// This entire page hurts to look at. export const Install = async (req, res) => { let data = req.body; - let name = data.name; + let { name, service_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 volumes = [volume0, volume1, volume2, volume3, volume4, volume5]; + let env_vars = [env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11]; + let labels = [label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11]; + let docker_volumes = []; + + // Make sure there isn't a container already running that has the same name let containers = await docker.listContainers({ all: true }); for (let i = 0; i < containers.length; i++) { if (containers[i].Names[0].includes(name)) { - addAlert(req.session, 'danger', `App ${name} already exists. Please remove it first.`); + addAlert(req.session, 'danger', `App '${name}' already exists. Please choose a different name.`); res.redirect('/'); return; } } - if (req.body.compose) { + // async function composeInstall (compose) { + // await compose.pull(); + // await compose.up(); + // } - mkdirSync(`./appdata/${name}`, { recursive: true }); - writeFileSync(`./templates/compose/${name}/compose.yaml`, req.body.compose, function (err) { console.log(err) }); - let compose = new DockerodeCompose(docker, `./templates/compose/${name}/compose.yaml`, `${name}`); - addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`); + // (async () => { + // await compose.pull().then(() => { + // compose.up(); + // }); + // })(); + + async function composeInstall (name, compose, req) { try { - (async () => { - await compose.pull(); - await compose.up(); + + await compose.pull().then(() => { + compose.up(); - await Syslog.create({ + Syslog.create({ user: req.session.user, email: null, event: "App Installation", - message: `${app} installed successfully`, + message: `${name} installed successfully`, ip: req.socket.remoteAddress - }); - })(); + }); + + }); + } catch (err) { + await Syslog.create({ user: req.session.user, email: null, event: "App Installation", - message: `${app} installation failed: ${err}`, + message: `${name} installation failed: ${err}`, ip: req.socket.remoteAddress }); + } - } else { - - let { service_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 = []; - - addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`); - - 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] + addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`); - for (let i = 0; i < volumes.length; i++) { - if (volumes[i] == 'on') { - compose_file += `\n volumes:` - break; - } - } + // Compose file installation + if (req.body.compose) { + // Create the directory + mkdirSync(`./appdata/${name}`, { recursive: true }); + // Write the form data to the compose file + writeFileSync(`./templates/compose/${name}/compose.yaml`, req.body.compose, function (err) { console.log(err) }); + var compose = new DockerodeCompose(docker, `./templates/compose/${name}/compose.yaml`, `${name}`); + composeInstall(name, compose, req); + res.redirect('/'); + return; + } - for (let i = 0; i < volumes.length; i++) { + // Convert a JSON template into a compose file + let compose_file = `version: '3'`; + compose_file += `\nservices:` + compose_file += `\n ${service_name}:` + compose_file += `\n container_name: ${name}`; + compose_file += `\n image: ${image}`; - // 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; - } - } + // 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}'` } - // add any docker volumes to the docker-compose file - if ( docker_volumes.length > 0 ) { - compose_file += `\n` - compose_file += `\nvolumes:` + // Restart policy + if (restart_policy != '') { compose_file += `\n restart: ${restart_policy}` } - // check docker_volumes for duplicates and remove them completely - docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index) + // 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 < docker_volumes.length; i++) { - if ( docker_volumes[i] != '') { - compose_file += `\n ${docker_volumes[i]}:` - } - } - } - + 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`]}` + } + } - try { - mkdirSync(`./appdata/${name}`, { recursive: true }); - writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) }); - var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`); - } catch { - await Syslog.create({ - user: req.session.user, - email: null, - event: "App Installation", - message: `${name} installation failed - error creating directory or compose file`, - ip: req.socket.remoteAddress - }); - } + // Volumes + for (let i = 0; i < volumes.length; i++) { + if (volumes[i] == 'on') { + compose_file += `\n volumes:` + break; + } + } - try { - (async () => { - await compose.pull(); - await compose.up(); + 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}`); + } + } - await Syslog.create({ - user: req.session.user, - email: null, - event: "App Installation", - message: `${name} installed successfully`, - ip: req.socket.remoteAddress - }); - })(); - } catch (err) { - await Syslog.create({ - user: req.session.user, - email: null, - event: "App Installation", - message: `${name} installation failed: ${err}`, - ip: req.socket.remoteAddress - }); + // Environment variables + 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 + 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 volumes to the compose file + if ( docker_volumes.length > 0 ) { + compose_file += `\n` + compose_file += `\nvolumes:` + // Removed any duplicates from docker_volumes + 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]}:` } } } + + mkdirSync(`./appdata/${name}`, { recursive: true }); + writeFileSync(`./appdata/${name}/compose.yaml`, compose_file, function (err) { console.log(err) }); + var compose = new DockerodeCompose(docker, `./appdata/${name}/compose.yaml`, `${name}`); + composeInstall(name, compose, req); + + res.redirect('/'); } + +// im just going to leave this old stackfile snippet here for now + +// 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') } +// } +// } \ No newline at end of file diff --git a/views/account.html b/views/account.html index 8df3680..6109bd1 100644 --- a/views/account.html +++ b/views/account.html @@ -44,77 +44,61 @@ <%- include('partials/sidebar.html') %>
This contact will be shown to others publicly, so choose it carefully.
-You can set a permanent password if you don't want to use temporary login codes.
- -Making your profile public means that anyone on the Dashkit network will be able to find - you.
-Set the default behaviour for container links.
+This contact will be shown to others publicly, so choose it carefully.
-You can set a permanent password if you don't want to use temporary login codes.
- -Making your profile public means that anyone on the Dashkit network will be able to find - you.
-