portal.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. import { Readable } from 'stream';
  2. import { Permission, Container, User } from '../database/models.js';
  3. import { docker } from '../server.js';
  4. import { readFileSync } from 'fs';
  5. let hidden = '';
  6. // The actual page
  7. export const Portal = (req, res) => {
  8. let name = req.session.user;
  9. let role = req.session.role;
  10. let avatar = name.charAt(0).toUpperCase();
  11. res.render("portal", {
  12. name: name,
  13. avatar: avatar,
  14. role: role
  15. });
  16. }
  17. async function CardList () {
  18. let name = req.session.user;
  19. let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
  20. // for (let i = 0; i < containers.length; i++) {
  21. // let details = await containerInfo(containers[i].containerName);
  22. // let card = await createCard(details);
  23. // cardList += card;
  24. // }
  25. for (let i = 0; i < containers.length; i++) {
  26. console.log(containers[i].containerName);
  27. }
  28. }
  29. export const UserContainers = async (req, res) => {
  30. let name = req.session.user;
  31. let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
  32. for (let i = 0; i < containers.length; i++) {
  33. if (containers[i].containerName == null) { continue; }
  34. let details = await containerInfo(containers[i].containerName);
  35. let card = await createCard(details);
  36. cardList += card;
  37. }
  38. res.send(cardList);
  39. }
  40. async function containerInfo (containerName) {
  41. let container = docker.getContainer(containerName);
  42. let info = await container.inspect();
  43. let image = info.Config.Image.split('/');
  44. let ports_list = [];
  45. try {
  46. for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
  47. let ports = {
  48. check: 'checked',
  49. external: value[0].HostPort,
  50. internal: key.split('/')[0],
  51. protocol: key.split('/')[1]
  52. }
  53. ports_list.push(ports);
  54. }
  55. } catch {
  56. // no exposed ports
  57. }
  58. let external = 0;
  59. let internal = 0;
  60. try {
  61. external = ports_list[0].external;
  62. internal = ports_list[0].internal;
  63. } catch {
  64. // no exposed ports
  65. }
  66. let details = {
  67. name: containerName,
  68. image: image,
  69. service: image[image.length - 1].split(':')[0],
  70. state: info.State.Status,
  71. external_port: external,
  72. internal_port: internal,
  73. ports: ports_list,
  74. link: 'localhost',
  75. }
  76. return details;
  77. }
  78. async function createCard (details) {
  79. if (hidden.includes(details.name)) { return;}
  80. let shortname = details.name.slice(0, 10) + '...';
  81. let trigger = 'data-hx-trigger="load, every 3s"';
  82. let state = details.state;
  83. let state_color = '';
  84. switch (state) {
  85. case 'running':
  86. state_color = 'green';
  87. break;
  88. case 'exited':
  89. state = 'stopped';
  90. state_color = 'red';
  91. trigger = 'data-hx-trigger="load"';
  92. break;
  93. case 'paused':
  94. state_color = 'orange';
  95. trigger = 'data-hx-trigger="load"';
  96. break;
  97. case 'installing':
  98. state_color = 'blue';
  99. trigger = 'data-hx-trigger="load"';
  100. break;
  101. }
  102. // if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
  103. let card = readFileSync('./views/partials/containerSimple.html', 'utf8');
  104. card = card.replace(/AppName/g, details.name);
  105. card = card.replace(/AppShortName/g, shortname);
  106. card = card.replace(/AppIcon/g, details.service);
  107. card = card.replace(/AppState/g, state);
  108. card = card.replace(/StateColor/g, state_color);
  109. card = card.replace(/ExternalPort/g, details.external_port);
  110. card = card.replace(/InternalPort/g, details.internal_port);
  111. card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
  112. card = card.replace(/AppNameState/g, `${details.name}State`);
  113. card = card.replace(/data-trigger=""/, trigger);
  114. return card;
  115. }
  116. let [ cardList, newCards, containersArray, sentArray, updatesArray ] = [ '', '', [], [], [] ];
  117. export async function addCard (name, state) {
  118. console.log(`Adding card for ${name}: ${state}`);
  119. let details = {
  120. name: name,
  121. image: name,
  122. service: name,
  123. state: 'installing',
  124. external_port: 0,
  125. internal_port: 0,
  126. ports: [],
  127. link: 'localhost',
  128. }
  129. createCard(details).then(card => {
  130. cardList += card;
  131. });
  132. }
  133. // HTMX server-side events
  134. export const SSE = async (req, res) => {
  135. res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
  136. let eventCheck = setInterval(async () => {
  137. // builds array of containers and their states
  138. containersArray = [];
  139. await docker.listContainers({ all: true }).then(containers => {
  140. containers.forEach(container => {
  141. let name = container.Names[0].replace('/', '');
  142. if (!hidden.includes(name)) { // if not hidden
  143. containersArray.push({ container: name, state: container.State });
  144. }
  145. });
  146. });
  147. if ((JSON.stringify(containersArray) !== JSON.stringify(sentArray))) {
  148. cardList = '';
  149. newCards = '';
  150. containersArray.forEach(container => {
  151. const { container: containerName, state } = container;
  152. const existingContainer = sentArray.find(c => c.container === containerName);
  153. if (!existingContainer) {
  154. containerInfo(containerName).then(details => {
  155. createCard(details).then(card => {
  156. newCards += card;
  157. });
  158. });
  159. res.write(`event: update\n`);
  160. res.write(`data: 'update cards'\n\n`);
  161. } else if (existingContainer.state !== state) {
  162. updatesArray.push(containerName);
  163. }
  164. containerInfo(containerName).then(details => {
  165. createCard(details).then(card => {
  166. cardList += card;
  167. });
  168. });
  169. });
  170. sentArray.forEach(container => {
  171. const { container: containerName } = container;
  172. const existingContainer = containersArray.find(c => c.container === containerName);
  173. if (!existingContainer) {
  174. updatesArray.push(containerName);
  175. }
  176. });
  177. for (let i = 0; i < updatesArray.length; i++) {
  178. res.write(`event: ${updatesArray[i]}\n`);
  179. res.write(`data: 'update cards'\n\n`);
  180. }
  181. updatesArray = [];
  182. sentArray = containersArray.slice();
  183. }
  184. }, 500);
  185. req.on('close', () => {
  186. clearInterval(eventCheck);
  187. });
  188. };
  189. export const updateCards = async (req, res) => {
  190. console.log('updateCards called');
  191. res.send(newCards);
  192. newCards = '';
  193. }
  194. export const Containers = async (req, res) => {
  195. CardList();
  196. // res.send(cardList);
  197. }
  198. export const Card = async (req, res) => {
  199. let name = req.header('hx-trigger-name');
  200. console.log(`${name} requesting updated card`);
  201. // return nothing if in hidden or not found in containersArray
  202. if (hidden.includes(name) || !containersArray.find(c => c.container === name)) {
  203. res.send('');
  204. return;
  205. } else {
  206. let details = await containerInfo(name);
  207. let card = await createCard(details);
  208. res.send(card);
  209. }
  210. }
  211. function status (state) {
  212. let status = `<span class="text-yellow align-items-center lh-1">
  213. <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
  214. ${state}
  215. </span>`;
  216. return status;
  217. }
  218. export const Logs = (req, res) => {
  219. let name = req.header('hx-trigger-name');
  220. function containerLogs (data) {
  221. return new Promise((resolve, reject) => {
  222. let logString = '';
  223. var options = { follow: false, stdout: true, stderr: false, timestamps: false };
  224. var containerName = docker.getContainer(data);
  225. containerName.logs(options, function (err, stream) {
  226. if (err) { reject(err); return; }
  227. const readableStream = Readable.from(stream);
  228. readableStream.on('data', function (chunk) {
  229. logString += chunk.toString('utf8');
  230. });
  231. readableStream.on('end', function () {
  232. resolve(logString);
  233. });
  234. });
  235. });
  236. };
  237. containerLogs(name).then((data) => {
  238. res.send(`<pre>${data}</pre> `)
  239. });
  240. }
  241. export const Action = async (req, res) => {
  242. let name = req.header('hx-trigger-name');
  243. let state = req.header('hx-trigger');
  244. let action = req.params.action;
  245. // Start
  246. if ((action == 'start') && (state == 'stopped')) {
  247. var containerName = docker.getContainer(name);
  248. containerName.start();
  249. res.send(status('starting'));
  250. } else if ((action == 'start') && (state == 'paused')) {
  251. var containerName = docker.getContainer(name);
  252. containerName.unpause();
  253. res.send(status('starting'));
  254. // Stop
  255. } else if ((action == 'stop') && (state != 'stopped')) {
  256. var containerName = docker.getContainer(name);
  257. containerName.stop();
  258. res.send(status('stopping'));
  259. // Pause
  260. } else if ((action == 'pause') && (state == 'paused')) {
  261. var containerName = docker.getContainer(name);
  262. containerName.unpause();
  263. res.send(status('starting'));
  264. } else if ((action == 'pause') && (state == 'running')) {
  265. var containerName = docker.getContainer(name);
  266. containerName.pause();
  267. res.send(status('pausing'));
  268. // Restart
  269. } else if (action == 'restart') {
  270. var containerName = docker.getContainer(name);
  271. containerName.restart();
  272. res.send(status('restarting'));
  273. // Hide
  274. } else if (action == 'hide') {
  275. let exists = await Container.findOne({ where: {name: name}});
  276. if (!exists) {
  277. const newContainer = await Container.create({ name: name, visibility: false, });
  278. } else {
  279. exists.update({ visibility: false });
  280. }
  281. hidden = await Container.findAll({ where: {visibility:false}});
  282. hidden = hidden.map((container) => container.name);
  283. res.send("ok");
  284. // Reset View
  285. } else if (action == 'reset') {
  286. await Container.update({ visibility: true }, { where: {} });
  287. hidden = await Container.findAll({ where: {visibility:false}});
  288. hidden = hidden.map((container) => container.name);
  289. res.send("ok");
  290. }
  291. }
  292. export const Modals = async (req, res) => {
  293. let name = req.header('hx-trigger-name');
  294. let id = req.header('hx-trigger');
  295. let title = name.charAt(0).toUpperCase() + name.slice(1);
  296. if (id == 'permissions') {
  297. let permissions_list = '';
  298. let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
  299. permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
  300. let users = await User.findAll({ attributes: ['username', 'UUID']});
  301. for (let i = 0; i < users.length; i++) {
  302. let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
  303. let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
  304. if (!exists) {
  305. const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID});
  306. }
  307. let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
  308. if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
  309. if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
  310. if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
  311. if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
  312. if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
  313. if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
  314. if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
  315. if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
  316. user_permissions = user_permissions.replace(/EntryNumber/g, i);
  317. user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
  318. user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
  319. permissions_list += user_permissions;
  320. }
  321. permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
  322. res.send(permissions_modal);
  323. return;
  324. }
  325. if (id == 'uninstall') {
  326. let modal = readFileSync('./views/modals/uninstall.html', 'utf8');
  327. modal = modal.replace(/AppName/g, name);
  328. // let containerPermissions = await Permission.findAll({ where: {containerName: name}});
  329. res.send(modal);
  330. return;
  331. }
  332. let modal = readFileSync('./views/modals/details.html', 'utf8');
  333. let details = await containerInfo(name);
  334. modal = modal.replace(/AppName/g, details.name);
  335. modal = modal.replace(/AppImage/g, details.image);
  336. res.send(modal);
  337. }