瀏覽代碼

Improved UI for apps.js and new template import

lllllllillllllillll 1 年之前
父節點
當前提交
62b7e73aac

+ 1 - 0
controllers/account.js

@@ -13,6 +13,7 @@ export const Account = async (req, res) => {
         email: user.email,
         role: user.role,
         avatar: user.avatar,
+        alert: '',
     });
 
 

+ 133 - 121
controllers/apps.js

@@ -1,148 +1,140 @@
 import { readFileSync } from 'fs';
+import multer from 'multer';
+
+const upload = multer({storage: multer.diskStorage({
+  destination: function (req, file, cb) {
+    cb(null, 'templates/')
+  },
+    filename: function (req, file, cb) {
+      cb(null, file.originalname)
+    }
+  })
+})
 
+// load the default template then sort the templates by name
 let templatesJSON = readFileSync('./templates/templates.json');
 let templates = JSON.parse(templatesJSON).templates;
-
 templates = templates.sort((a, b) => {
     if (a.name < b.name) {
       return -1;
     }
 });
 
-
+let alert = '';
 
 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);
-
-    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'];
-        }
-
-        for (let c = 0; c < templates[i].categories.length; c++) {
-          categories += CatagoryColor(templates[i].categories[c]);
-        }
-
-        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", {
-        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
-    });
-
-}
 
-
-
-export const appSearch = async (req, res) => {
-
-    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 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);
-    }
-
-    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]);
-            }
-        }
-    }
-    searchTemplates(search);
-
-    for (let i = 0; i < results.length; i++) {
+  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 appCard = readFileSync('./views/partials/appCard.html', 'utf8');
-      let name = results[i].name || results[i].title.toLowerCase();
-      let desc = results[i].description.slice(0, 60) + "...";
-      let description = results[i].description.replaceAll(". ", ".\n") || "no description available";
-      let note = results[i].note ? results[i].note.replaceAll(". ", ".\n") : "no notes available";
-      let image = results[i].image;
-      let logo = results[i].logo;
-
+      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 (results[i].categories == null || results[i].categories == undefined || results[i].categories == '') {
-          results[i].categories = ['Other'];
+      // 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'];
       }
-
-      for (let c = 0; c < results[i].categories.length; c++) {
-        categories += CatagoryColor(results[i].categories[c]);
+      // 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;
+  }
+  // let templatesJSON = readFileSync('./templates/templates.json');
+  // let templates = JSON.parse(templatesJSON).templates;
+
+  res.render("apps", {
+    name: req.session.user,
+    role: req.session.role,
+    avatar: req.session.user.charAt(0).toUpperCase(),
+    list_start: list_start + 1,
+    list_end: list_end,
+    app_count: templates.length,
+    prev: prev,
+    next: next,
+    apps_list: apps_list,
+    alert: alert || ''
+  });
+  alert = '';
+}
+
+export const appSearch = async (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);
+  let prev = '/apps/' + (page-1);
+  let next = '/apps/' + (page+1);
+  if (page == 1) { prev = '/apps/' + (page); }
+  if (page == last_page) { next = '/apps/' + (page); }
+
+  let search = req.body.search.split(' ');
+  let apps_list = '';
+  let results = [];
+
+  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]);
+      }
     }
-    
-    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: results.length,
-        prev: prev,
-        next: next,
-        apps_list: apps_list
-    });
+  }
+  searchTemplates(search);
+
+  for (let i = 0; i < results.length; i++) {
+    let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
+    let name = results[i].name || results[i].title.toLowerCase();
+    let desc = results[i].description.slice(0, 60) + "...";
+    let description = results[i].description.replaceAll(". ", ".\n") || "no description available";
+    let note = results[i].note ? results[i].note.replaceAll(". ", ".\n") : "no notes available";
+    let image = results[i].image;
+    let logo = results[i].logo;let categories = '';
+    // set data.catagories to 'other' if data.catagories is empty or undefined
+    if (results[i].categories == null || results[i].categories == undefined || results[i].categories == '') {
+      results[i].categories = ['Other'];
+    }
+    // loop through the categories and add the badge to the card
+    for (let j = 0; j < results[i].categories.length; j++) {
+      categories += CatagoryColor(results[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", {
+      name: req.session.user,
+      role: req.session.role,
+      avatar: req.session.avatar,
+      list_start: list_start + 1,
+      list_end: list_end,
+      app_count: results.length,
+      prev: prev,
+      next: next,
+      apps_list: apps_list,
+      alert: res.locals.alert || ''
+  });
 }
 
 
@@ -389,4 +381,24 @@ export const LearnMore = async (req, res) => {
 export const ImportModal = async (req, res) => {
   let modal = readFileSync('./views/modals/import.html', 'utf8');
   res.send(modal);
-}
+}
+
+
+export const Upload = (req, res) => {
+  upload.array('files', 10)(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>
+              <a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
+            </div>`;
+    
+    res.redirect('/apps');
+  });
+};

+ 2 - 1
controllers/dashboard.js

@@ -17,7 +17,8 @@ export const Dashboard = (req, res) => {
     res.render("dashboard", {
         name: name,
         avatar: avatar,
-        role: role
+        role: role,
+        alert: ''
     });
 }
 

+ 2 - 1
controllers/images.js

@@ -50,7 +50,8 @@ export const Images = async function(req, res) {
         role: req.session.role,
         avatar: req.session.avatar,
         image_list: image_list,
-        image_count: images.length
+        image_count: images.length,
+        alert: '',
     });
 
 }

+ 2 - 1
controllers/networks.js

@@ -43,7 +43,8 @@ export const Networks = async function(req, res) {
         role: req.session.role,
         avatar: req.session.avatar,
         network_list: network_list,
-        network_count: networks.length
+        network_count: networks.length,
+        alert: '',
     });
 }
 

+ 8 - 7
controllers/portal.js

@@ -24,20 +24,21 @@ export const Portal = (req, res) => {
 async function CardList () {
     let name = req.session.user;
     let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
-    // for (let i = 0; i < containers.length; i++) {
-    //     let details = await containerInfo(containers[i].containerName);
-    //     let card = await createCard(details);
-    //     cardList += card;
-    // }
-
     for (let i = 0; i < containers.length; i++) {
-        console.log(containers[i].containerName);
+        let details = await containerInfo(containers[i].containerName);
+        let card = await createCard(details);
+        cardList += card;
     }
+
+    // for (let i = 0; i < containers.length; i++) {
+    //     console.log(containers[i].containerName);
+    // }
     
 
 }
 
 export const UserContainers = async (req, res) => {
+    let cardList = '';
     let name = req.session.user;
     let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
 

+ 1 - 0
controllers/settings.js

@@ -5,5 +5,6 @@ export const Settings = (req, res) => {
         name: req.session.user,
         role: req.session.role,
         avatar: req.session.avatar,
+        alert: '',
     });
 }

+ 1 - 0
controllers/supporters.js

@@ -13,6 +13,7 @@ export const Supporters = async (req, res) => {
         email: user.email,
         role: user.role,
         avatar: user.avatar,
+        alert: '',
     });
 
 

+ 2 - 1
controllers/syslogs.js

@@ -30,7 +30,8 @@ export const Syslogs = async function(req, res) {
         name: req.session.user || 'Dev',
         role: req.session.role || 'Dev',
         avatar: req.session.avatar || '<img src="/img/avatars/rus.jpg">',
-        logs: logs
+        logs: logs,
+        alert: '',
     });
 
 }

+ 1 - 1
controllers/users.js

@@ -44,7 +44,7 @@ export const Users = async (req, res) => {
             <td>${account.role}</td>
             <td>${account.lastLogin}</td>
             <td>${active}</td>
-            <td><a href="#" class="btn">Edit</a></td>
+            <td><a href="#" class="btn">View</a></td>
         </tr>`
 
         user_list += info;

+ 2 - 1
controllers/volumes.js

@@ -64,7 +64,8 @@ export const Volumes = async function(req, res) {
         role: req.session.role,
         avatar: req.session.avatar,
         volume_list: volume_list,
-        volume_count: volumes.length
+        volume_count: volumes.length,
+        alert: '',
     });
 
 }

+ 127 - 0
package-lock.json

@@ -17,6 +17,7 @@
         "express-session": "^1.18.0",
         "js-yaml": "^4.1.0",
         "memorystore": "^1.6.7",
+        "multer": "^1.4.5-lts.1",
         "sequelize": "^6.37.1",
         "sqlite3": "^5.1.7",
         "systeminformation": "^5.22.6"
@@ -186,6 +187,11 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/append-field": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+      "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
+    },
     "node_modules/aproba": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
@@ -357,6 +363,11 @@
         "ieee754": "^1.1.13"
       }
     },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "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",
@@ -366,6 +377,17 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/busboy": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+      "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+      "dependencies": {
+        "streamsearch": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      }
+    },
     "node_modules/bytes": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -500,6 +522,47 @@
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
     },
+    "node_modules/concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+      "engines": [
+        "node >= 0.8"
+      ],
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "node_modules/concat-stream/node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/concat-stream/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+    },
+    "node_modules/concat-stream/node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
     "node_modules/console-control-strings": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@@ -537,6 +600,11 @@
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
       "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
     },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "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",
@@ -1313,6 +1381,11 @@
       "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
       "optional": true
     },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+    },
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -1665,6 +1738,34 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
+    "node_modules/multer": {
+      "version": "1.4.5-lts.1",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
+      "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
+      "dependencies": {
+        "append-field": "^1.0.0",
+        "busboy": "^1.0.0",
+        "concat-stream": "^1.5.2",
+        "mkdirp": "^0.5.4",
+        "object-assign": "^4.1.1",
+        "type-is": "^1.6.4",
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "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",
@@ -1924,6 +2025,11 @@
         "node": ">=10"
       }
     },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+    },
     "node_modules/promise-inflight": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
@@ -2454,6 +2560,14 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/streamsearch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+      "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/string_decoder": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -2636,6 +2750,11 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
+    },
     "node_modules/uid-safe": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
@@ -2765,6 +2884,14 @@
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
     },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
     "node_modules/yallist": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
     "express-session": "^1.18.0",
     "js-yaml": "^4.1.0",
     "memorystore": "^1.6.7",
+    "multer": "^1.4.5-lts.1",
     "sequelize": "^6.37.1",
     "sqlite3": "^5.1.7",
     "systeminformation": "^5.22.6"

+ 5 - 5
public/css/tabler.min.css

@@ -6036,7 +6036,7 @@ fieldset:disabled .btn {
   color: var(--tblr-alert-color);
   background-color: var(--tblr-alert-bg);
   border: var(--tblr-alert-border);
-  border-radius: var(--tblr-alert-border-radius)
+  border-radius: var(--tblr-alert-border-radius);
 }
 
 .alert-heading {
@@ -20221,11 +20221,11 @@ body[data-bs-theme=dark] .hide-theme-dark {
 }
 
 .alert {
-  --tblr-alert-color: var(--tblr-muted);
-  background: #fff;
+  --tblr-alert-color: var(--tblr-secondary);
+  --tblr-alert-bg: var(--tblr-surface);
   border: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);
-  border-left: .25rem var(--tblr-border-style) var(--tblr-alert-color);
-  box-shadow: rgba(24, 36, 51, .04) 0 2px 4px 0
+  border-left: 0.25rem var(--tblr-border-style) var(--tblr-alert-color);
+  box-shadow: rgba(24, 36, 51, 0.04) 0 2px 4px 0;
 }
 
 .alert>:last-child {

+ 22 - 24
router/index.js

@@ -7,7 +7,7 @@ export const router = express.Router();
 import { Login, submitLogin, Logout } from "../controllers/login.js";
 import { Register, submitRegister } from "../controllers/register.js";
 import { Dashboard, Logs, Modals, Stats, Chart, SSE, Card, updateCards, Containers, Action, UpdatePermissions } from "../controllers/dashboard.js";
-import { Apps, appSearch, InstallModal, ImportModal, LearnMore } from "../controllers/apps.js";
+import { Apps, appSearch, InstallModal, ImportModal, LearnMore, Upload } from "../controllers/apps.js";
 import { Users } from "../controllers/users.js";
 import { Images, removeImage } from "../controllers/images.js";
 import { Networks, removeNetwork } from "../controllers/networks.js";
@@ -30,34 +30,31 @@ const auth = async (req, res, next) => {
     // console.log("Auth: ", user, role, path, trigger, req.path);
 
     if (!user) { res.redirect('/login'); return; }
-    else if (role == "admin") { next(); return; }
-    else if (path == "/portal" || path == "/account" || path == "/supporters" || path == "/thank" || path == "/user_containers") { next(); return; }
-    else { res.redirect('/portal'); return; }
+    else if (role == 'admin' || path == "/portal" || path == "/account" || path == "/supporters" || path == "/thank" || path == "/user_containers") { next(); return; }
+    // else { res.redirect('/portal'); return; }
     
 
-    // let action = req.path.split("/")[2];
+    let action = req.path.split("/")[2];
 
-    // else if (trigger == "portal" || "supporters" || "account" || "thank") { res.redirect() return; }
 
-
-    // if (action == "start" || action == "stop" || action == "pause" || action == "restart") {
-    //     let permission = await Permission.findOne({ where: { containerName: trigger, user: user }, attributes: [`${action}`] });
+    if (action == "start" || action == "stop" || action == "pause" || action == "restart") {
+        let permission = await Permission.findOne({ where: { containerName: trigger, user: user }, attributes: [`${action}`] });
         
-    //     if (permission) {
-    //         if (permission[action] == true) {
-    //             console.log(`User ${user} has permission to ${action} ${trigger}`);
-    //             next();
-    //         }
-    //         else {
-    //             console.log(`User ${user} does not have permission to ${action} ${trigger}`);
-    //         }
-    //     } else {
-    //         console.log(`No entry found for ${user} in ${trigger} permissions`);
-    //     }
-    // }
-    // else {
-    //     res.redirect('/portal');
-    // }
+        if (permission) {
+            if (permission[action] == true) {
+                console.log(`User ${user} has permission to ${action} ${trigger}`);
+                next();
+            }
+            else {
+                console.log(`User ${user} does not have permission to ${action} ${trigger}`);
+            }
+        } else {
+            console.log(`No entry found for ${user} in ${trigger} permissions`);
+        }
+    }
+    else {
+        res.redirect('/portal');
+    }
 }
 
 
@@ -93,6 +90,7 @@ router.post("/apps", auth, appSearch);
 router.get("/install_modal", auth, InstallModal)
 router.get("/import_modal", auth, ImportModal)
 router.get("/learn_more", auth, LearnMore)
+router.post("/upload", auth, Upload);
 
 router.get("/users", auth, Users);
 router.get("/syslogs", auth, Syslogs);

+ 53 - 27
views/apps.html

@@ -27,38 +27,65 @@
 
       <div class="page-wrapper">
         <!-- Page header -->
-        <div class="mt-3 d-print-none">
+        <div class="mt-3">
           <div class="container-xl">
-            <div class="row g-2 align-items-center">
-              <div class="col">
-                <label class="text-secondary"><%= list_start %> - <%= list_end %> of <%= app_count %> Apps.</label>
+            <div class="row row-cards">
+
+              <div class="col-md-6 col-lg-3">
+                <div class="card">
+                  <div class="card-body text-center">
+                    <div class="d-flex align-items-center">
+                      <div class="me-auto"><%= list_start %> - <%= list_end %> of <%= app_count %> Apps</div>
+                      <button class="btn btn-primary" 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>
+                    </div>
+                  </div>
+                </div>
+              </div>
+
+              <div class="col-md-6 col-lg-3">
+                <div class="card">
+                  <div class="card-body text-center">
+                    <div class="d-flex align-items-center">
+                      <select class="form-select">
+                        <option>All</option>
+                        <option>Media</option>
+                        <option>Tools</option>
+                        <option>Gaming</option>
+                        <option>FOSS</option>
+                        <option>Database</option>
+                      </select>
+                    </div>
+                  </div>
+                </div>
               </div>
-              <!-- Page title actions -->
-              <div class="col-auto ms-auto d-print-none">
-                <div class="d-flex align-items-center">
-                  <div class="me-2">
-                    <select class="form-select">
-                      <option>Category:</option>
-                      <option>Media</option>
-                      <option>Tools</option>
-                      <option>Gaming</option>
-                      <option>FOSS</option>
-                      <option>Database</option>
-                    </select>
+      
+              <div class="col-md-6 col-lg-3">
+                <div class="card">
+                  <div class="card-body text-center">
+                    <div class="d-flex align-items-center">
+                      <select class="form-select">
+                        <option>Templates.json (default)</option>
+                        <option>Compose</option>
+                      </select>
+                    </div>
                   </div>
-                  <div class="me-2">
-                    <select class="form-select">
-                      <option>Templates.json (default)</option>
-                      <option>Compose</option>
-                    </select>
+                </div>
+              </div>
+      
+              <div class="col-md-6 col-lg-3">
+                <div class="card">
+                  <div class="card-body text-center">
+                    <div class="text-secondary d-flex align-items-center">
+                      <form action="/apps" id="search" name="search" method="POST" class="d-flex">
+                        <input type="search" class="form-control me-2" name="search" placeholder="Search apps…" >
+                        <input type="submit" form="search" class="btn btn-primary" value="Search">
+                      </form>
+                    </div>
                   </div>
-                  <button class="btn btn-primary h-50" 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>
-                  <form action="/apps" id="search" name="search" method="POST">
-                    <input type="search" class="form-control" name="search" placeholder="Search apps…" >
-                  </form>
-                  <input type="submit" form="search" class="btn btn-primary h-50" value="Search">
                 </div>
               </div>
+
+
             </div>
           </div>
         </div>
@@ -69,7 +96,6 @@
 
               <%- apps_list %>
 
-
             <!-- HTMX Target-->
             <div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
               <div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">

+ 6 - 4
views/modals/import.html

@@ -3,18 +3,20 @@
     <div class="modal-content">
         <div class="modal-body">
             <div class="modal-title">Import Template(s)</div>
-            <div class="text-muted">Templates can be *.json, *.yml, or .yaml</div>
+            <div class="text-muted">Template(s) can be *.json, *.yml, or *.yaml</div>
 
             <div class="mt-3">
-                <div class="form-label">Choose file(s)</div>
-                <input type="file" class="form-control">
+                <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>
             </div>
 
         </div>
         
         <div class="modal-footer">
             <button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
-            <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Upload</button>
+            <button type="submit" class="btn btn-primary" data-bs-dismiss="modal" form="upload">Upload</button>
         </div>
     </div>
 </div>

+ 1 - 1
views/partials/appCard.html

@@ -1,6 +1,6 @@
 <div class="col-md-6 col-lg-3">
     <div class="card">
-        <div class="card-body p-4 text-center">
+        <div class="card-body text-center">
             <span class="avatar avatar-xlplus mb-3 rounded"><img src='AppLogo' width="144px" height="144px" loading="lazy"/></span>
             <h3 class="m-0 mb-1"><a href="#">AppShortName</a></h3>
             <div class="text-secondary">AppDesc</div>

+ 1 - 1
views/partials/containerSimple.html

@@ -48,7 +48,7 @@
           </div>
         </div>
         <div class="d-flex align-items-baseline">
-          <div class="h1 me-2" title="AppName" style="margin-bottom: 0;">
+          <div class="h1 me-2" title="AppName">
             <a href="http://${link}:${external_port}" target="_blank">
               AppShortName
             </a>

+ 7 - 3
views/partials/navbar.html

@@ -27,7 +27,7 @@
     }
   }
 </script>
-<header class="navbar navbar-expand-md d-print-none">
+<header class="navbar navbar-expand-md d-print-none py-0">
   <div class="container-xl">
     <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu"
       aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
@@ -40,13 +40,17 @@
       <a href="#">
         <img src="/images/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">     
       </a>
-      
-
     </h1>
+
+    <% if(alert) { %>
+        <%- alert %>
+    <% } %>
+
     <div class="navbar-nav flex-row order-md-last">
       <div class="nav-item d-none d-md-flex me-3">
 
 
+    
         <!-- <div class="btn-list">
           <a href="#" class="btn text-green">
             <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-lock" 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 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z"></path> <path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path> <path d="M8 11v-4a4 4 0 1 1 8 0v4"></path> </svg>