From a73a89b2508688d3df78d5a6744a78d38a7421c4 Mon Sep 17 00:00:00 2001 From: lllllllillllllillll Date: Tue, 26 Mar 2024 20:38:57 -0700 Subject: [PATCH] mostly working permission system. --- controllers/dashboard.js | 22 +- controllers/portal.js | 299 +++++++++++++++++++++++++- controllers/supporters.js | 2 - database/models.js | 34 ++- router/index.js | 60 ++++-- views/partials/navbar.html | 2 +- views/partials/user_permissions.html | 16 +- views/portal.html | 310 +++++++++++++++++++-------- 8 files changed, 606 insertions(+), 139 deletions(-) diff --git a/controllers/dashboard.js b/controllers/dashboard.js index 55c8018..6869675 100644 --- a/controllers/dashboard.js +++ b/controllers/dashboard.js @@ -9,13 +9,15 @@ let hidden = ''; // The actual page export const Dashboard = (req, res) => { - - if (!req.session.user) { res.redirect("/logout"); return; } + let name = req.session.user; + let role = req.session.role; + let avatar = name.charAt(0).toUpperCase(); + res.render("dashboard", { - name: req.session.user, - role: req.session.role, - avatar: req.session.avatar, + name: name, + avatar: avatar, + role: role }); } @@ -393,6 +395,16 @@ export const Modals = async (req, res) => { const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID}); } + let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}}); + if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); } + if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); } + if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); } + if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); } + if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); } + if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); } + if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); } + if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); } + user_permissions = user_permissions.replace(/EntryNumber/g, i); user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username); user_permissions = user_permissions.replace(/PermissionsContainer/g, name); diff --git a/controllers/portal.js b/controllers/portal.js index 604f86c..4725440 100644 --- a/controllers/portal.js +++ b/controllers/portal.js @@ -1,16 +1,299 @@ +import { Readable } from 'stream'; +import { Permission, Container, 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'; +let hidden = ''; + +// The actual page export const Portal = (req, res) => { + + let name = req.session.user; + let role = req.session.role; + let avatar = name.charAt(0).toUpperCase(); - if (!req.session.user) { - res.redirect("/login"); + res.render("portal", { + name: name, + avatar: avatar, + role: role + }); +} + +async function containerInfo (containerName) { + let container = docker.getContainer(containerName); + let info = await container.inspect(); + let image = info.Config.Image.split('/'); + let ports_list = []; + try { + for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) { + let ports = { + check: 'checked', + external: value[0].HostPort, + internal: key.split('/')[0], + protocol: key.split('/')[1] + } + ports_list.push(ports); + } + } catch { + // no exposed ports + } + + let external = 0; + let internal = 0; + try { + external = ports_list[0].external; + internal = ports_list[0].internal; + } catch { + // no exposed ports + } + + + let details = { + name: containerName, + image: image, + service: image[image.length - 1].split(':')[0], + state: info.State.Status, + external_port: external, + internal_port: internal, + ports: ports_list, + link: 'localhost', + } + return details; +} + +async function createCard (details) { + if (hidden.includes(details.name)) { return;} + let shortname = details.name.slice(0, 10) + '...'; + let trigger = 'data-hx-trigger="load, every 3s"'; + let state = details.state; + let state_color = ''; + switch (state) { + case 'running': + state_color = 'green'; + break; + case 'exited': + state = 'stopped'; + state_color = 'red'; + trigger = 'data-hx-trigger="load"'; + break; + case 'paused': + state_color = 'orange'; + trigger = 'data-hx-trigger="load"'; + break; + case 'installing': + state_color = 'blue'; + trigger = 'data-hx-trigger="load"'; + break; + } + // if (name.startsWith('dweebui')) { disable = 'disabled=""'; } + let card = readFileSync('./views/partials/containerCard.html', 'utf8'); + card = card.replace(/AppName/g, details.name); + card = card.replace(/AppShortName/g, shortname); + card = card.replace(/AppIcon/g, details.service); + card = card.replace(/AppState/g, state); + card = card.replace(/StateColor/g, state_color); + card = card.replace(/ExternalPort/g, details.external_port); + card = card.replace(/InternalPort/g, details.internal_port); + card = card.replace(/ChartName/g, details.name.replace(/-/g, '')); + card = card.replace(/AppNameState/g, `${details.name}State`); + card = card.replace(/data-trigger=""/, trigger); + return card; +} + + +let [ cardList, newCards, containersArray, sentArray, updatesArray ] = [ '', '', [], [], [] ]; + +export async function addCard (name, state) { + console.log(`Adding card for ${name}: ${state}`); + + let details = { + name: name, + image: name, + service: name, + state: 'installing', + external_port: 0, + internal_port: 0, + ports: [], + link: 'localhost', + + } + createCard(details).then(card => { + cardList += card; + }); +} + + + +export const updateCards = async (req, res) => { + console.log('updateCards called'); + res.send(newCards); + newCards = ''; +} + + +export const Containers = async (req, res) => { + res.send(cardList); +} + +export const Card = async (req, res) => { + let name = req.header('hx-trigger-name'); + console.log(`${name} requesting updated card`); + // return nothing if in hidden or not found in containersArray + if (hidden.includes(name) || !containersArray.find(c => c.container === name)) { + res.send(''); + return; + } else { + let details = await containerInfo(name); + let card = await createCard(details); + res.send(card); + } +} + + + + +function status (state) { + let status = ` + + ${state} + `; + return status; +} + + +export const Logs = (req, res) => { + let name = req.header('hx-trigger-name'); + function containerLogs (data) { + return new Promise((resolve, reject) => { + let logString = ''; + var options = { follow: false, stdout: true, stderr: false, timestamps: false }; + var containerName = docker.getContainer(data); + containerName.logs(options, function (err, stream) { + if (err) { reject(err); return; } + const readableStream = Readable.from(stream); + readableStream.on('data', function (chunk) { + logString += chunk.toString('utf8'); + }); + readableStream.on('end', function () { + resolve(logString); + }); + }); + }); + }; + containerLogs(name).then((data) => { + res.send(`
${data}
`) + }); +} + +export const Action = async (req, res) => { + let name = req.header('hx-trigger-name'); + let state = req.header('hx-trigger'); + let action = req.params.action; + // Start + if ((action == 'start') && (state == 'stopped')) { + var containerName = docker.getContainer(name); + containerName.start(); + res.send(status('starting')); + } else if ((action == 'start') && (state == 'paused')) { + var containerName = docker.getContainer(name); + containerName.unpause(); + res.send(status('starting')); + // Stop + } else if ((action == 'stop') && (state != 'stopped')) { + var containerName = docker.getContainer(name); + containerName.stop(); + res.send(status('stopping')); + // Pause + } else if ((action == 'pause') && (state == 'paused')) { + var containerName = docker.getContainer(name); + containerName.unpause(); + res.send(status('starting')); + } else if ((action == 'pause') && (state == 'running')) { + var containerName = docker.getContainer(name); + containerName.pause(); + res.send(status('pausing')); + // Restart + } else if (action == 'restart') { + var containerName = docker.getContainer(name); + containerName.restart(); + res.send(status('restarting')); + // Hide + } else if (action == 'hide') { + let exists = await Container.findOne({ where: {name: name}}); + if (!exists) { + const newContainer = await Container.create({ name: name, visibility: false, }); + } else { + exists.update({ visibility: false }); + } + hidden = await Container.findAll({ where: {visibility:false}}); + hidden = hidden.map((container) => container.name); + res.send("ok"); + // Reset View + } else if (action == 'reset') { + await Container.update({ visibility: true }, { where: {} }); + hidden = await Container.findAll({ where: {visibility:false}}); + hidden = hidden.map((container) => container.name); + res.send("ok"); + } +} + + +export const Modals = async (req, res) => { + let name = req.header('hx-trigger-name'); + let id = req.header('hx-trigger'); + let title = name.charAt(0).toUpperCase() + name.slice(1); + + if (id == 'permissions') { + let permissions_list = ''; + let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8'); + permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title); + let users = await User.findAll({ attributes: ['username', 'UUID']}); + + for (let i = 0; i < users.length; i++) { + let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8'); + let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}}); + if (!exists) { + const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID}); + } + + let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}}); + if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); } + if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); } + if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); } + if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); } + if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); } + if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); } + if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); } + if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); } + + user_permissions = user_permissions.replace(/EntryNumber/g, i); + user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username); + user_permissions = user_permissions.replace(/PermissionsContainer/g, name); + + permissions_list += user_permissions; + } + + permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list); + res.send(permissions_modal); return; } - res.render("portal", { - name: req.session.user, - role: req.session.role, - avatar: req.session.avatar, - }); -} + if (id == 'uninstall') { + let modal = readFileSync('./views/modals/uninstall.html', 'utf8'); + modal = modal.replace(/AppName/g, name); + // let containerPermissions = await Permission.findAll({ where: {containerName: name}}); + res.send(modal); + return; + } + + let modal = readFileSync('./views/modals/details.html', 'utf8'); + let details = await containerInfo(name); + + modal = modal.replace(/AppName/g, details.name); + modal = modal.replace(/AppImage/g, details.image); + res.send(modal); +} \ No newline at end of file diff --git a/controllers/supporters.js b/controllers/supporters.js index d7bc9c0..bcd6135 100644 --- a/controllers/supporters.js +++ b/controllers/supporters.js @@ -2,8 +2,6 @@ import { User } from "../database/models.js"; export const Supporters = async (req, res) => { - if (!req.session.UUID) return res.redirect("/login"); - let user = await User.findOne({ where: { UUID: req.session.UUID }}); diff --git a/database/models.js b/database/models.js index c12ad50..1f70ae1 100644 --- a/database/models.js +++ b/database/models.js @@ -124,39 +124,51 @@ export const Permission = sequelize.define('Permission', { }, install: { type: DataTypes.STRING, + defaultValue: false }, uninstall: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, edit: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, upgrade: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, start: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, stop: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, restart: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, pause: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, logs: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, hide: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, reset_view: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, view: { - type: DataTypes.STRING + type: DataTypes.STRING, + defaultValue: false }, }); diff --git a/router/index.js b/router/index.js index b13f4fd..299b06b 100644 --- a/router/index.js +++ b/router/index.js @@ -1,5 +1,5 @@ import express from "express"; -import { Permission, User } from '../database/models.js'; +import { Permission } from '../database/models.js'; export const router = express.Router(); @@ -20,28 +20,54 @@ import { Syslogs } from "../controllers/syslogs.js"; import { Portal } from "../controllers/portal.js" // Auth middleware -const auth = (req, res, next) => { - if (req.session.role == "admin") { +const auth = async (req, res, next) => { + if (!req.session.user) { res.redirect('/login'); return; } + + let user = req.session.user; + let role = req.session.role; + let action = req.path.split("/")[2]; + let trigger = req.header('hx-trigger-name'); + // console.log("Auth: ", user, role, action, trigger); + + if (role == "admin") { next(); - } else { - res.redirect("/portal"); } -}; + else 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'); + } + +} // Admin routes router.get("/", auth, Dashboard); router.post("/action/:action", auth, Action); - -router.get("/logs", auth, Logs); -router.get("/modals", auth, Modals); -router.get("/stats", auth, Stats); -router.get("/chart", auth, Chart); -router.get("/sse_event", auth, SSE); -router.get("/containers", auth, Containers); -router.get("/card", auth, Card); -router.get("/new_cards", auth, updateCards); router.post("/updatePermissions", auth, UpdatePermissions); +router.get("/logs", Logs); +router.get("/modals", Modals); +router.get("/stats", Stats); +router.get("/chart", Chart); +router.get("/sse_event", SSE); +router.get("/containers", Containers); +router.get("/card", Card); +router.get("/new_cards", updateCards); + + router.get("/images", auth, Images); router.post("/removeImage", auth, removeImage); @@ -81,6 +107,6 @@ router.get("/logout", Logout); import { Install } from "../functions/install.js" import { Uninstall } from "../functions/uninstall.js" -router.post("/install", auth, Install); -router.post("/uninstall", auth, Uninstall); +router.post("/install", Install); +router.post("/uninstall", Uninstall); diff --git a/views/partials/navbar.html b/views/partials/navbar.html index 6a33451..a23beb6 100644 --- a/views/partials/navbar.html +++ b/views/partials/navbar.html @@ -138,7 +138,7 @@
@@ -70,7 +70,7 @@
@@ -85,7 +85,7 @@
@@ -100,7 +100,7 @@
@@ -115,7 +115,7 @@
@@ -130,7 +130,7 @@
@@ -146,7 +146,7 @@
diff --git a/views/portal.html b/views/portal.html index 22363e8..5a12f81 100644 --- a/views/portal.html +++ b/views/portal.html @@ -8,6 +8,7 @@ + -
+ +
- <%- include('partials/navbar.html') %> -
- -
-
-
- -
-
- -
-
-
-
-
- - - + <%- include('partials/navbar.html') %> + +
+ +
+
+
+ +
+
+ +
+
+
+
+
+ + + +
+ + +
+
+
-
-
- -
-
- -
+
+
+
- -
-
-
-
-
- - - +
+ +
+
+
+
+
+ + + +
+ + +
+
+
-
-
- -
-
- -
+
+
+
- -
-
-
-
-
- - - +
+ +
+
+
+
+
+ + + +
+ + +
+
+
-
-
- -
-
- -
+
+
+
- -
-
-
-
-
- - - +
+ +
+
+
+
+
+ + + +
+ + +
+
+
-
-
- -
-
- -
+
+
+
- - +
+ + +
+
+ + + +
+
+
+
+ + +
+
+
+
+ + + + + + +
- - <%- include('partials/footer.html') %> -
+ + <%- include('partials/footer.html') %> +
+
- - - + + + + + +