ソースを参照

added compose file import

lllllllillllllillll 1 年間 前
コミット
c71f330b49
11 ファイル変更651 行追加202 行削除
  1. 6 2
      CHANGELOG.md
  2. 27 27
      compose.yaml
  3. 93 52
      controllers/apps.js
  4. 8 7
      controllers/dashboard.js
  5. 9 17
      controllers/login.js
  6. 493 83
      package-lock.json
  7. 3 2
      package.json
  8. BIN
      public/img/add to zip.jpg
  9. 6 7
      server.js
  10. 3 3
      views/apps.html
  11. 3 2
      views/modals/import.html

+ 6 - 2
CHANGELOG.md

@@ -7,9 +7,13 @@
 * Container cards display pending actions (starting, stopping, pausing, restarting).
 * Dynamically generated avatars.
 * Updated database models.
-* Persistent Database.
-* Install cards displayed on dashboard.
+* Multi-user permission system.
+* Refactored dashboard to support multiple users.
+* New alert banner for installs and file uploads.
 * Updated docker compose to include HTTPS Env.
+* Updated Apps page to view or upload compose files.
+* Improved app search.
+* Updated dependencies.
 
 ## v0.40 (Feb 26th 2024) - HTMX rewrite
 * Pages rewritten to use HTMX.

+ 27 - 27
docker-compose.yaml → compose.yaml

@@ -1,28 +1,28 @@
-version: "3.9"
-services:
-  dweebui:
-    container_name: dweebui
-    image: lllllllillllllillll/dweebui:v0.50
-    environment:
-      PORT: 8000
-      SECRET: MrWiskers
-      HTTPS: false
-    restart: unless-stopped
-    ports:
-      - 8000:8000
-    volumes:
-      - dweebui:/app/database/
-      # Docker socket
-      - /var/run/docker.sock:/var/run/docker.sock
-      # Podman socket
-      #- /run/podman/podman.sock:/var/run/docker.sock
-
-    networks:
-      - dweebui_net
-
-volumes:
-  dweebui:
-
-networks:
-  dweebui_net:
+version: "3.9"
+services:
+  dweebui:
+    container_name: dweebui
+    image: lllllllillllllillll/dweebui:v0.60-dev
+    environment:
+      PORT: 8000
+      SECRET: MrWiskers
+      HTTPS: false
+    restart: unless-stopped
+    ports:
+      - 8000:8000
+    volumes:
+      - dweebui:/app
+      # Docker socket
+      - /var/run/docker.sock:/var/run/docker.sock
+      # Podman socket
+      #- /run/podman/podman.sock:/var/run/docker.sock
+
+    networks:
+      - dweebui_net
+
+volumes:
+  dweebui:
+
+networks:
+  dweebui_net:
     driver: bridge

+ 93 - 52
controllers/apps.js

@@ -1,5 +1,7 @@
-import { readFileSync, readdirSync, renameSync, mkdirSync, unlinkSync } from 'fs';
+import { readFileSync, readdirSync, renameSync, mkdirSync, unlinkSync, read } from 'fs';
+import { parse } from 'yaml';
 import multer from 'multer';
+import AdmZip from 'adm-zip';
 
 const upload = multer({storage: multer.diskStorage({
   destination: function (req, file, cb) { cb(null, 'templates/tmp/') },
@@ -7,55 +9,85 @@ const upload = multer({storage: multer.diskStorage({
 })});
 
 let alert = '';
+let templates_global = '';
 
-export const Apps = (req, res) => {
+export const Apps = async (req, res) => {
   
   let page = Number(req.params.page) || 1;
   let template_param = req.params.template || 'default';
 
-  let template_file = readFileSync(`./templates/json/${template_param}.json`);
+  let apps_list = '';
+  let app_count = '';
+  
+  let list_start = (page - 1) * 28;
+  let list_end = (page * 28);
+  let last_page = '';
 
-  let templates = JSON.parse(template_file).templates;
+  let prev = '/apps/' + (page - 1) + '/' + template_param;
+  let next = '/apps/' + (page + 1) + '/' + template_param;
+  if (page == 1) { prev = '/apps/' + (page) + '/' + template_param; }
+  if (page == last_page) { next = '/apps/' + (page) + '/' + template_param;}
 
-  templates = templates.sort((a, b) => {
-      if (a.name < b.name) { return -1; }
-  });
 
+  if (template_param == 'compose') {
+    let compose_files = readdirSync('templates/compose/');
+    
+    app_count = compose_files.length;
+    last_page = Math.ceil(compose_files.length/28);
 
+    compose_files.forEach(file => {
+      let compose = readFileSync(`templates/compose/${file}/compose.yaml`, 'utf8');
+      let compose_data = parse(compose);
+      let service_name = Object.keys(compose_data.services)
+      let container = compose_data.services[service_name].container_name;
+      let image = compose_data.services[service_name].image;
 
-  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 appCard = readFileSync('./views/partials/appCard.html', 'utf8');
-      let name = templates[i].name || templates[i].title.toLowerCase();
-      let desc = templates[i].description.slice(0, 60) + "...";
-      let description = templates[i].description.replaceAll(". ", ".\n") || "no description available";
-      let note = templates[i].note ? templates[i].note.replaceAll(". ", ".\n") : "no notes available";
-      let image = templates[i].image;
-      let logo = templates[i].logo;
-      let categories = '';
-      // set data.catagories to 'other' if data.catagories is empty or undefined
-      if (templates[i].categories == null || templates[i].categories == undefined || templates[i].categories == '') {
-          templates[i].categories = ['Other'];
-      }
-      // loop through the categories and add the badge to the card
-      for (let j = 0; j < templates[i].categories.length; j++) {
-        categories += CatagoryColor(templates[i].categories[j]);
-      }
-      appCard = appCard.replace(/AppName/g, name);
-      appCard = appCard.replace(/AppShortName/g, name);
-      appCard = appCard.replace(/AppDesc/g, desc);
-      appCard = appCard.replace(/AppLogo/g, logo);
-      appCard = appCard.replace(/AppCategories/g, categories);
+      appCard = appCard.replace(/AppName/g, service_name);
+      appCard = appCard.replace(/AppShortName/g, service_name);
+      appCard = appCard.replace(/AppDesc/g, 'Compose File');
+      appCard = appCard.replace(/AppLogo/g, `https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service_name}.png`);
+      appCard = appCard.replace(/AppCategories/g, '<span class="badge bg-orange-lt">Compose</span> ');
       apps_list += appCard;
-  }
+    });
+  } else {
+
+      let template_file = readFileSync(`./templates/json/default.json`);
+      let templates = JSON.parse(template_file).templates;
+      templates = templates.sort((a, b) => { if (a.name < b.name) { return -1; } });
+      app_count = templates.length; 
+
+      templates_global = templates;
+
+      apps_list = '';
+      for (let i = list_start; i < list_end && i < templates.length; i++) {
+          let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
+          let name = templates[i].name || templates[i].title.toLowerCase();
+          let desc = templates[i].description.slice(0, 60) + "...";
+          let description = templates[i].description.replaceAll(". ", ".\n") || "no description available";
+          let note = templates[i].note ? templates[i].note.replaceAll(". ", ".\n") : "no notes available";
+          let image = templates[i].image;
+          let logo = templates[i].logo;
+          let categories = '';
+          // set data.catagories to 'other' if data.catagories is empty or undefined
+          if (templates[i].categories == null || templates[i].categories == undefined || templates[i].categories == '') {
+              templates[i].categories = ['Other'];
+          }
+          // loop through the categories and add the badge to the card
+          for (let j = 0; j < templates[i].categories.length; j++) {
+            categories += CatagoryColor(templates[i].categories[j]);
+          }
+          appCard = appCard.replace(/AppName/g, name);
+          appCard = appCard.replace(/AppShortName/g, name);
+          appCard = appCard.replace(/AppDesc/g, desc);
+          appCard = appCard.replace(/AppLogo/g, logo);
+          appCard = appCard.replace(/AppCategories/g, categories);
+          apps_list += appCard;
+      }
+    }
+
+  
+    
 
 
   res.render("apps", {
@@ -64,7 +96,7 @@ export const Apps = (req, res) => {
     avatar: req.session.user.charAt(0).toUpperCase(),
     list_start: list_start + 1,
     list_end: list_end,
-    app_count: templates.length,
+    app_count: app_count,
     prev: prev,
     next: next,
     apps_list: apps_list,
@@ -212,7 +244,7 @@ function CatagoryColor(category) {
 
 export const InstallModal = async (req, res) => {
   let input = req.header('hx-trigger-name');
-  let result = templates.find(t => t.name == input);
+  let result = templates_global.find(t => t.name == input);
 
   let name = result.name || result.title.toLowerCase();
   let short_name = name.slice(0, 25) + "...";
@@ -416,12 +448,8 @@ export const Upload = (req, res) => {
 
     alert = `<div class="alert alert-success alert-dismissible mb-0 py-2" role="alert">
               <div class="d-flex">
-                <div>
-                  <svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" 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="M5 12l5 5l10 -10"></path></svg>
-                </div>
-                <div>
-                  Template(s) Uploaded!
-                </div>
+                <div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" 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="M5 12l5 5l10 -10"></path></svg> </div>
+                <div>Template(s) Uploaded!</div>
               </div>
               <a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
             </div>`;
@@ -429,16 +457,29 @@ export const Upload = (req, res) => {
     let files = readdirSync('templates/tmp/');
 
     for (let i = 0; i < files.length; i++) {
-      if (files[i].endsWith('.json')) {
+      if (files[i].endsWith('.zip')) {
+        let zip = new AdmZip(`templates/tmp/${files[i]}`);
+        zip.extractAllTo('templates/compose', true);
+        unlinkSync(`templates/tmp/${files[i]}`);
+      } else if (files[i].endsWith('.json')) {
         renameSync(`templates/tmp/${files[i]}`, `templates/json/${files[i]}`);
-      } else if (files[i].endsWith('.yml') || files[i].endsWith('.yaml')) {
-        mkdirSync(`templates/compose/${files[i].slice(0, -4)}`);
-        renameSync(`templates/tmp/${files[i]}`, `templates/compose/${files[i].slice(0, -4)}/${files[i]}`);
+      } else if (files[i].endsWith('.yml')) {
+        let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
+        let compose_data = parse(compose);
+        let service_name = Object.keys(compose_data.services)
+        mkdirSync(`templates/compose/${service_name}`);
+        renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
+      } else if (files[i].endsWith('.yaml')) {
+        let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
+        let compose_data = parse(compose);
+        let service_name = Object.keys(compose_data.services)
+        mkdirSync(`templates/compose/${service_name}`);
+        renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
       } else {
+        console.log('Unsupported file type');
         unlinkSync(`templates/tmp/${files[i]}`);
       }
-    }
-        
+    }   
     res.redirect('/apps');
   });
 };

+ 8 - 7
controllers/dashboard.js

@@ -12,6 +12,7 @@ let [ cardList, newCards, stats ] = [ '', '', {}];
 
 // The page
 export const Dashboard = (req, res) => {
+
     let name = req.session.user;
     let role = req.session.role;
     alert = req.session.alert;
@@ -338,15 +339,15 @@ export const Stats = async (req, res) => {
 export async function addAlert (session, type, message) {
     session.alert = `<div class="alert alert-${type} alert-dismissible py-2 mb-0" role="alert" id="alert">
                         <div class="d-flex">
-                        <div class="spinner-border text-info nav-link">
-                            <span class="visually-hidden">Loading...</span>
-                        </div>
-                        <div>
-                            ${message}
-                        </div>
+                            <div class="spinner-border text-info nav-link">
+                                <span class="visually-hidden">Loading...</span>
+                            </div>
+                            <div>
+                              ${message}
+                            </div>
                         </div>
                         <button class="btn-close" data-hx-post="/dashboard/alert" data-hx-trigger="click" data-hx-target="#alert" data-hx-swap="outerHTML" style="padding-top: 0.5rem;" ></button>
-                        </div>`;
+                    </div>`;
 }
 
 export const UpdatePermissions = async (req, res) => {

+ 9 - 17
controllers/login.js

@@ -2,31 +2,22 @@ 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":"",
-        });
-    }
+    if (req.session.user) { res.redirect("/logout"); }
+    else { res.render("login",{ "error":"", }); }
 }
 
 export const submitLogin = async function(req,res){
-
     let { email, password } = req.body;
     email = email.toLowerCase();
 
-    if(email && password){
-
+    if (email && password) {
         let existingUser = await User.findOne({ where: {email:email}});
-        if(existingUser){
+        if (existingUser) {
 
             let match = await bcrypt.compare(password,existingUser.password);
 
-            if(match){
-
+            if (match) {
                 let currentDate = new Date();
                 let newLogin = currentDate.toLocaleString();
                 await User.update({lastLogin: newLogin}, {where: {UUID:existingUser.UUID}});
@@ -43,8 +34,9 @@ export const submitLogin = async function(req,res){
                     message: "User logged in successfully",
                     ip: req.socket.remoteAddress
                 });
+                
                 res.redirect("/dashboard");
-            }else{
+            } else {
 
                 const syslog = await Syslog.create({
                     user: null,
@@ -58,12 +50,12 @@ export const submitLogin = async function(req,res){
                     "error":"Invalid password",
                 });
             }
-        }else{
+        } else {
             res.render("login",{
                 "error":"User with that email does not exist.",
             });
         }
-    }else{
+    } else {
         res.status(400);
         res.render("login",{
             "error":"Please fill in all the fields.",

+ 493 - 83
package-lock.json

@@ -9,18 +9,19 @@
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {
+        "adm-zip": "^0.5.12",
         "bcrypt": "^5.1.1",
         "dockerode": "^4.0.2",
         "dockerode-compose": "^1.4.0",
         "ejs": "^3.1.10",
         "express": "^4.19.2",
         "express-session": "^1.18.0",
-        "js-yaml": "^4.1.0",
         "memorystore": "^1.6.7",
         "multer": "^1.4.5-lts.1",
         "sequelize": "^6.37.3",
         "sqlite3": "^5.1.7",
-        "systeminformation": "^5.22.7"
+        "systeminformation": "^5.22.7",
+        "yaml": "^2.4.2"
       }
     },
     "node_modules/@balena/dockerignore": {
@@ -34,6 +35,68 @@
       "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
       "optional": true
     },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+    },
+    "node_modules/@isaacs/cliui/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
     "node_modules/@mapbox/node-pre-gyp": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@@ -77,6 +140,27 @@
         "node": ">=10"
       }
     },
+    "node_modules/@npmcli/move-file/node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "optional": true,
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/@tootallnate/once": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -100,9 +184,9 @@
       "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
     },
     "node_modules/@types/node": {
-      "version": "20.11.20",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
-      "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
+      "version": "20.12.10",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz",
+      "integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==",
       "dependencies": {
         "undici-types": "~5.26.4"
       }
@@ -129,6 +213,14 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/adm-zip": {
+      "version": "0.5.12",
+      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
+      "integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==",
+      "engines": {
+        "node": ">=6.0"
+      }
+    },
     "node_modules/agent-base": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -368,15 +460,6 @@
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
       "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
     },
-    "node_modules/buildcheck": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
-      "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
-      "optional": true,
-      "engines": {
-        "node": ">=10.0.0"
-      }
-    },
     "node_modules/busboy": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -425,6 +508,26 @@
         "node": ">= 10"
       }
     },
+    "node_modules/cacache/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "optional": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/cacache/node_modules/lru-cache": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -437,6 +540,18 @@
         "node": ">=10"
       }
     },
+    "node_modules/cacache/node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "optional": true,
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/cacache/node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -605,18 +720,17 @@
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
       "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
     },
-    "node_modules/cpu-features": {
-      "version": "0.0.9",
-      "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz",
-      "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==",
-      "hasInstallScript": true,
-      "optional": true,
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
       "dependencies": {
-        "buildcheck": "~0.0.6",
-        "nan": "^2.17.0"
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
       },
       "engines": {
-        "node": ">=10.0.0"
+        "node": ">= 8"
       }
     },
     "node_modules/debug": {
@@ -696,9 +810,9 @@
       }
     },
     "node_modules/detect-libc": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
-      "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+      "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
       "engines": {
         "node": ">=8"
       }
@@ -761,6 +875,11 @@
       "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
       "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
     },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+    },
     "node_modules/ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -1029,6 +1148,32 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
       "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
     },
+    "node_modules/foreground-child": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
+      "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/foreground-child/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/forwarded": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1117,24 +1262,56 @@
       "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
     },
     "node_modules/glob": {
-      "version": "7.2.3",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
-      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "version": "10.3.12",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
+      "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
       "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.1.1",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^2.3.6",
+        "minimatch": "^9.0.1",
+        "minipass": "^7.0.4",
+        "path-scurry": "^1.10.2"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
       },
       "engines": {
-        "node": "*"
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "9.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+      "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/glob/node_modules/minipass": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.0.tgz",
+      "integrity": "sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
     "node_modules/gopd": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -1389,13 +1566,29 @@
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-      "optional": true
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+    },
+    "node_modules/jackspeak": {
+      "version": "2.3.6",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
+      "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
     },
     "node_modules/jake": {
-      "version": "10.8.7",
-      "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
-      "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
+      "version": "10.9.1",
+      "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz",
+      "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==",
       "dependencies": {
         "async": "^3.2.3",
         "chalk": "^4.0.2",
@@ -1699,14 +1892,14 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
     "node_modules/mkdirp": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "version": "0.5.6",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+      "dependencies": {
+        "minimist": "^1.2.6"
+      },
       "bin": {
         "mkdirp": "bin/cmd.js"
-      },
-      "engines": {
-        "node": ">=10"
       }
     },
     "node_modules/mkdirp-classic": {
@@ -1755,23 +1948,6 @@
         "node": ">= 6.0.0"
       }
     },
-    "node_modules/multer/node_modules/mkdirp": {
-      "version": "0.5.6",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
-      "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
-      "dependencies": {
-        "minimist": "^1.2.6"
-      },
-      "bin": {
-        "mkdirp": "bin/cmd.js"
-      }
-    },
-    "node_modules/nan": {
-      "version": "2.18.0",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
-      "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
-      "optional": true
-    },
     "node_modules/napi-build-utils": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
@@ -1786,9 +1962,9 @@
       }
     },
     "node_modules/node-abi": {
-      "version": "3.55.0",
-      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.55.0.tgz",
-      "integrity": "sha512-uPEjtyh2tFEvWYt4Jw7McOD5FPcHkcxm/tHZc5PWaDB3JYq0rGFUbgaAK+CT5pYpQddBfsZVWI08OwoRfdfbcQ==",
+      "version": "3.62.0",
+      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz",
+      "integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==",
       "dependencies": {
         "semver": "^7.3.5"
       },
@@ -1876,6 +2052,26 @@
         "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
       }
     },
+    "node_modules/node-gyp/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "optional": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/node-gyp/node_modules/npmlog": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
@@ -1990,20 +2186,59 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
+      "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "10.2.2",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
+      "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
+      "engines": {
+        "node": "14 || >=16.14"
+      }
+    },
+    "node_modules/path-scurry/node_modules/minipass": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.0.tgz",
+      "integrity": "sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
     "node_modules/path-to-regexp": {
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
       "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
     },
     "node_modules/pg-connection-string": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
-      "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
+      "version": "2.6.4",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz",
+      "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA=="
     },
     "node_modules/prebuild-install": {
-      "version": "7.1.1",
-      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
-      "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
+      "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
       "dependencies": {
         "detect-libc": "^2.0.0",
         "expand-template": "^2.0.3",
@@ -2174,6 +2409,25 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/rimraf/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/safe-buffer": {
       "version": "5.2.1",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -2378,6 +2632,25 @@
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
       "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
     },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/side-channel": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
@@ -2454,9 +2727,9 @@
       }
     },
     "node_modules/socks": {
-      "version": "2.8.1",
-      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz",
-      "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==",
+      "version": "2.8.3",
+      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
+      "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
       "optional": true,
       "dependencies": {
         "ip-address": "^9.0.5",
@@ -2589,6 +2862,20 @@
         "node": ">=8"
       }
     },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-ansi": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -2600,6 +2887,18 @@
         "node": ">=8"
       }
     },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-json-comments": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
@@ -2620,9 +2919,9 @@
       }
     },
     "node_modules/systeminformation": {
-      "version": "5.22.7",
-      "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.7.tgz",
-      "integrity": "sha512-AWxlP05KeHbpGdgvZkcudJpsmChc2Y5Eo/GvxG/iUA/Aws5LZKHAMSeAo+V+nD+nxWZaxrwpWcnx4SH3oxNL3A==",
+      "version": "5.22.8",
+      "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.8.tgz",
+      "integrity": "sha512-F1iWQ+PSfOzvLMGh2UXASaWLDq5o+1h1db13Kddl6ojcQ47rsJhpMtRrmBXfTA5QJgutC4KV67YRmXLuroIxrA==",
       "os": [
         "darwin",
         "linux",
@@ -2645,9 +2944,9 @@
       }
     },
     "node_modules/tar": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
-      "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
       "dependencies": {
         "chownr": "^2.0.0",
         "fs-minipass": "^2.0.0",
@@ -2699,6 +2998,17 @@
         "node": ">=8"
       }
     },
+    "node_modules/tar/node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/tar/node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -2852,7 +3162,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
       "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-      "optional": true,
       "dependencies": {
         "isexe": "^2.0.0"
       },
@@ -2879,6 +3188,96 @@
         "@types/node": "*"
       }
     },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+    },
+    "node_modules/wrap-ansi/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -2896,6 +3295,17 @@
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
       "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
+    },
+    "node_modules/yaml": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
+      "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
     }
   }
 }

+ 3 - 2
package.json

@@ -12,17 +12,18 @@
   "author": "",
   "license": "ISC",
   "dependencies": {
+    "adm-zip": "^0.5.12",
     "bcrypt": "^5.1.1",
     "dockerode": "^4.0.2",
     "dockerode-compose": "^1.4.0",
     "ejs": "^3.1.10",
     "express": "^4.19.2",
     "express-session": "^1.18.0",
-    "js-yaml": "^4.1.0",
     "memorystore": "^1.6.7",
     "multer": "^1.4.5-lts.1",
     "sequelize": "^6.37.3",
     "sqlite3": "^5.1.7",
-    "systeminformation": "^5.22.7"
+    "systeminformation": "^5.22.7",
+    "yaml": "^2.4.2"
   }
 }

BIN
public/img/add to zip.jpg


+ 6 - 7
server.js

@@ -9,8 +9,7 @@ export const docker = new Docker();
 
 const app = express();
 const MemoryStore = memorystore(session);
-const port = process.env.PORT || 8000;
-const connection = process.env.HTTPS || false;
+const PORT = process.env.PORT || 8000;
 
 // Session middleware
 const sessionMiddleware = session({
@@ -19,9 +18,9 @@ const sessionMiddleware = session({
     resave: false, 
     saveUninitialized: false, 
     cookie:{
-        secure: connection, 
-        httpOnly: connection,
-        maxAge:3600000 * 8 // Session max age in milliseconds. 3600000 = 1 hour.
+        secure: false, 
+        httpOnly: false,
+        maxAge: 3600000 * 8 // Session max age in milliseconds. 3600000 = 1 hour.
     }
 });
 
@@ -36,7 +35,7 @@ app.use([
 ]);
 
 // Initialize server
-app.listen(port, async () => {
+app.listen(PORT, async () => {
     async function init() {// I made sure the console.logs and emojis lined up
         try { await sequelize.authenticate().then(
             () => { console.log('DB Connection: ✔️') }); }
@@ -45,6 +44,6 @@ app.listen(port, async () => {
             () => { console.log('Synced Models: ✔️') }); }
             catch { console.log('Synced Models: ❌'); } }
         await init().then(() => { 
-            console.log(`Listening on http://localhost:${port}`);
+            console.log(`Listening on http://localhost:${PORT}`);
     });
 });

+ 3 - 3
views/apps.html

@@ -79,10 +79,10 @@
                   <div class="card-body text-center">
                     <div class="d-flex align-items-center">
                       <dropdown class="me-2">
-                        <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Templates.json</button>
+                        <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Default Templates</button>
                         <ul class="dropdown-menu">
-                          <li><a class="dropdown-item" href="#">Templates.json</a></li>
-                          <li><a class="dropdown-item" href="#">Compose</a></li>
+                          <li><a class="dropdown-item" href="/apps">Default Templates</a></li>
+                          <li><a class="dropdown-item" href="/apps/1/compose">Compose Files</a></li>
                         </ul>
                       </dropdown>
                       <button class="btn" name="Import" id="Import" data-hx-get="/import_modal" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Import</button>

+ 3 - 2
views/modals/import.html

@@ -3,9 +3,10 @@
     <div class="modal-content">
         <div class="modal-body">
             <div class="modal-title">Import Template(s)</div>
-            <div class="text-muted">Template(s) can be *.json, *.yml, or *.yaml</div>
+            <div class="text-muted mb-2">Template(s) can be *.json, *.yml, or *.yaml.</div>
+            <div class="text-muted mb-3">Multiple compose files can be imported from a zip file.</div>
+            <img src = "/img/add to zip.jpg" alt = "Add to zip" class = "img-fluid" />
             <div class="mt-3">
-                <div class="form-label">Choose file(s):</div>
                 <form method="post" action="/upload" enctype="multipart/form-data" id="upload">
                     <input type="file" name="files" multiple />
                 </form>